From bc110ce60916b40a5bc7ef3e91d9da1b8751e93b Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Thu, 7 Jan 2021 22:40:25 +0100 Subject: [PATCH] Hard: LTV index refactor (#758) * add set/delete/update ltv methods * refactor borrow logic * basic updates to keeper logic for compile * Add deposit index set/delete/update keeper methods * refactor deposit logic * refactor repay logic * update withdraw logic * introduce DeleteDepositBorrowAndLtvIndex * remove unused bool from AttemptKeeperLiquidation * remove comments (transitioned to asana cards) * catch multiple error types in liquidation loop --- x/hard/handler.go | 2 +- x/hard/keeper/borrow.go | 17 +++++-- x/hard/keeper/deposit.go | 13 +++-- x/hard/keeper/liquidation.go | 82 ++++++++++++++----------------- x/hard/keeper/liquidation_test.go | 4 +- x/hard/keeper/repay.go | 15 ++++-- x/hard/keeper/withdraw.go | 12 +++-- 7 files changed, 80 insertions(+), 65 deletions(-) diff --git a/x/hard/handler.go b/x/hard/handler.go index 1d98746c..4d4d6c71 100644 --- a/x/hard/handler.go +++ b/x/hard/handler.go @@ -124,7 +124,7 @@ func handleMsgRepay(ctx sdk.Context, k keeper.Keeper, msg types.MsgRepay) (*sdk. } func handleMsgLiquidate(ctx sdk.Context, k keeper.Keeper, msg types.MsgLiquidate) (*sdk.Result, error) { - _, err := k.AttemptKeeperLiquidation(ctx, msg.Keeper, msg.Borrower) + err := k.AttemptKeeperLiquidation(ctx, msg.Keeper, msg.Borrower) if err != nil { return nil, err } diff --git a/x/hard/keeper/borrow.go b/x/hard/keeper/borrow.go index 03bf89c8..f1d05522 100644 --- a/x/hard/keeper/borrow.go +++ b/x/hard/keeper/borrow.go @@ -23,7 +23,7 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins } // Get current stored LTV based on stored borrows/deposits - prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, borrower) + prevLtv, err := k.GetStoreLTV(ctx, borrower) if err != nil { return err } @@ -85,11 +85,20 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins amount = coins } - // Update the borrower's amount and borrow interest factors in the store + // Construct the user's new/updated borrow with amount and interest factors borrow := types.NewBorrow(borrower, amount, borrowInterestFactors) - k.SetBorrow(ctx, borrow) - k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, borrower) + // Calculate the new Loan-to-Value ratio of Deposit-to-Borrow + deposit, foundDeposit := k.GetDeposit(ctx, borrower) + if !foundDeposit { + return types.ErrDepositNotFound + } + newLtv, err := k.CalculateLtv(ctx, deposit, borrow) + if err != nil { + return err + } + + k.UpdateBorrowAndLtvIndex(ctx, borrow, newLtv, prevLtv) // Update total borrowed amount by newly borrowed coins. Don't add user's pending interest as // it has already been included in the total borrowed coins by the BeginBlocker. diff --git a/x/hard/keeper/deposit.go b/x/hard/keeper/deposit.go index 91f6d0a7..c540875c 100644 --- a/x/hard/keeper/deposit.go +++ b/x/hard/keeper/deposit.go @@ -24,7 +24,7 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi } // Get current stored LTV based on stored borrows/deposits - prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor) + prevLtv, err := k.GetStoreLTV(ctx, depositor) if err != nil { return err } @@ -87,12 +87,17 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi } 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) + // Calculate the new Loan-to-Value ratio of Deposit-to-Borrow + borrow, _ := k.GetBorrow(ctx, depositor) + newLtv, err := k.CalculateLtv(ctx, deposit, borrow) + if err != nil { + return err + } + + k.UpdateDepositAndLtvIndex(ctx, deposit, newLtv, prevLtv) // 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. diff --git a/x/hard/keeper/liquidation.go b/x/hard/keeper/liquidation.go index a4130c46..f3a3a45d 100644 --- a/x/hard/keeper/liquidation.go +++ b/x/hard/keeper/liquidation.go @@ -22,9 +22,9 @@ func (k Keeper) AttemptIndexLiquidations(ctx sdk.Context) error { borrowers := k.GetLtvIndexSlice(ctx, params.CheckLtvIndexCount) for _, borrower := range borrowers { - _, err := k.AttemptKeeperLiquidation(ctx, sdk.AccAddress(types.LiquidatorAccount), borrower) + err := k.AttemptKeeperLiquidation(ctx, sdk.AccAddress(types.LiquidatorAccount), borrower) if err != nil { - if !errors.Is(err, types.ErrBorrowNotLiquidatable) { + if !errors.Is(err, types.ErrBorrowNotLiquidatable) && !errors.Is(err, types.ErrBorrowNotFound) { panic(err) } } @@ -33,34 +33,30 @@ func (k Keeper) AttemptIndexLiquidations(ctx sdk.Context) error { } // AttemptKeeperLiquidation enables a keeper to liquidate an individual borrower's position -func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, borrower sdk.AccAddress) (bool, error) { - prevLtv, shouldInsertIndex, err := k.GetStoreLTV(ctx, borrower) +func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, borrower sdk.AccAddress) error { + prevLtv, err := k.GetStoreLTV(ctx, borrower) if err != nil { - return false, err + return err } k.SyncBorrowInterest(ctx, borrower) - k.UpdateItemInLtvIndex(ctx, prevLtv, shouldInsertIndex, borrower) - - // Fetch deposits and parse coin denoms deposit, found := k.GetDeposit(ctx, borrower) if !found { - return false, sdkerrors.Wrapf(types.ErrDepositsNotFound, "no deposits found for %s", borrower) + return types.ErrDepositNotFound } - // Fetch borrow balances and parse coin denoms borrow, found := k.GetBorrow(ctx, borrower) if !found { - return false, types.ErrBorrowNotFound + return types.ErrBorrowNotFound } isWithinRange, err := k.IsWithinValidLtvRange(ctx, deposit, borrow) if err != nil { - return false, err + return err } if isWithinRange { - return false, sdkerrors.Wrapf(types.ErrBorrowNotLiquidatable, "position is within valid LTV range") + return sdkerrors.Wrapf(types.ErrBorrowNotLiquidatable, "position is within valid LTV range") } // Sending coins to auction module with keeper address getting % of the profits @@ -68,19 +64,11 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, depositDenoms := getDenoms(deposit.Amount) err = k.SeizeDeposits(ctx, keeper, deposit, borrow, depositDenoms, borrowDenoms) if err != nil { - return false, err + return err } - currLtv, _, err := k.GetStoreLTV(ctx, borrower) - if err != nil { - return false, err - } - k.RemoveFromLtvIndex(ctx, currLtv, borrower) - - k.DeleteBorrow(ctx, borrow) - k.DeleteDeposit(ctx, deposit) - - return true, err + k.DeleteDepositBorrowAndLtvIndex(ctx, deposit, borrow, prevLtv) + return nil } // SeizeDeposits seizes a list of deposits and sends them to auction @@ -276,36 +264,40 @@ func (k Keeper) IsWithinValidLtvRange(ctx sdk.Context, deposit types.Deposit, bo return true, nil } -// UpdateItemInLtvIndex updates the key a borrower's address is stored under in the LTV index -func (k Keeper) UpdateItemInLtvIndex(ctx sdk.Context, prevLtv sdk.Dec, - shouldRemoveIndex bool, borrower sdk.AccAddress) error { - currLtv, shouldInsertIndex, err := k.GetStoreLTV(ctx, borrower) - if err != nil { - return err - } +// UpdateBorrowAndLtvIndex updates a borrow and its LTV index value in the store +func (k Keeper) UpdateBorrowAndLtvIndex(ctx sdk.Context, borrow types.Borrow, newLtv, oldLtv sdk.Dec) { + k.RemoveFromLtvIndex(ctx, oldLtv, borrow.Borrower) + k.SetBorrow(ctx, borrow) + k.InsertIntoLtvIndex(ctx, newLtv, borrow.Borrower) +} - if shouldRemoveIndex { - k.RemoveFromLtvIndex(ctx, prevLtv, borrower) - } - if shouldInsertIndex { - k.InsertIntoLtvIndex(ctx, currLtv, borrower) - } - return nil +// UpdateDepositAndLtvIndex updates a deposit and its LTV index value in the store +func (k Keeper) UpdateDepositAndLtvIndex(ctx sdk.Context, deposit types.Deposit, newLtv, oldLtv sdk.Dec) { + k.RemoveFromLtvIndex(ctx, oldLtv, deposit.Depositor) + k.SetDeposit(ctx, deposit) + k.InsertIntoLtvIndex(ctx, newLtv, deposit.Depositor) +} + +// DeleteDepositBorrowAndLtvIndex deletes deposit, borrow, and ltv index +func (k Keeper) DeleteDepositBorrowAndLtvIndex(ctx sdk.Context, deposit types.Deposit, borrow types.Borrow, oldLtv sdk.Dec) { + k.RemoveFromLtvIndex(ctx, oldLtv, deposit.Depositor) + k.DeleteDeposit(ctx, deposit) + k.DeleteBorrow(ctx, borrow) } // GetStoreLTV calculates the user's current LTV based on their deposits/borrows in the store // and does not include any outsanding interest. -func (k Keeper) GetStoreLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, bool, error) { +func (k Keeper) GetStoreLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, error) { // Fetch deposits and parse coin denoms deposit, found := k.GetDeposit(ctx, addr) if !found { - return sdk.ZeroDec(), false, nil + return sdk.ZeroDec(), nil } // Fetch borrow balances and parse coin denoms borrow, found := k.GetBorrow(ctx, addr) if !found { - return sdk.ZeroDec(), false, nil + return sdk.ZeroDec(), nil } return k.CalculateLtv(ctx, deposit, borrow) @@ -313,11 +305,11 @@ func (k Keeper) GetStoreLTV(ctx sdk.Context, addr sdk.AccAddress) (sdk.Dec, bool // CalculateLtv calculates the potential LTV given a user's deposits and borrows. // The boolean returned indicates if the LTV should be added to the store's LTV index. -func (k Keeper) CalculateLtv(ctx sdk.Context, deposit types.Deposit, borrow types.Borrow) (sdk.Dec, bool, error) { +func (k Keeper) CalculateLtv(ctx sdk.Context, deposit types.Deposit, borrow types.Borrow) (sdk.Dec, error) { // Load required liquidation data for every deposit/borrow denom liqMap, err := k.LoadLiquidationData(ctx, deposit, borrow) if err != nil { - return sdk.ZeroDec(), false, nil + return sdk.ZeroDec(), nil } // Build valuation map to hold deposit coin USD valuations @@ -339,11 +331,11 @@ func (k Keeper) CalculateLtv(ctx sdk.Context, deposit types.Deposit, borrow type // User doesn't have any deposits, catch divide by 0 error sumDeposits := depositCoinValues.Sum() if sumDeposits.Equal(sdk.ZeroDec()) { - return sdk.ZeroDec(), false, nil + return sdk.ZeroDec(), nil } // Loan-to-Value ratio - return borrowCoinValues.Sum().Quo(sumDeposits), true, nil + return borrowCoinValues.Sum().Quo(sumDeposits), nil } // LoadLiquidationData returns liquidation data, deposit, borrow diff --git a/x/hard/keeper/liquidation_test.go b/x/hard/keeper/liquidation_test.go index 8db6f226..7aafac3c 100644 --- a/x/hard/keeper/liquidation_test.go +++ b/x/hard/keeper/liquidation_test.go @@ -1358,9 +1358,8 @@ func (suite *KeeperTestSuite) TestKeeperLiquidation() { suite.Require().True(foundDepositBefore) // Attempt to liquidate - liquidated, err := suite.keeper.AttemptKeeperLiquidation(liqCtx, tc.args.keeper, tc.args.borrower) + err = suite.keeper.AttemptKeeperLiquidation(liqCtx, tc.args.keeper, tc.args.borrower) if tc.errArgs.expectPass { - suite.Require().True(liquidated) suite.Require().NoError(err) // Check borrow does not exist after liquidation @@ -1383,7 +1382,6 @@ func (suite *KeeperTestSuite) TestKeeperLiquidation() { suite.Require().True(len(auctions) > 0) suite.Require().Equal(tc.args.expectedAuctions, auctions) } else { - suite.Require().False(liquidated) suite.Require().Error(err) suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) diff --git a/x/hard/keeper/repay.go b/x/hard/keeper/repay.go index 239962c4..ad27498d 100644 --- a/x/hard/keeper/repay.go +++ b/x/hard/keeper/repay.go @@ -10,7 +10,7 @@ import ( // Repay borrowed funds func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { // Get current stored LTV based on stored borrows/deposits - prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, sender) + prevLtv, err := k.GetStoreLTV(ctx, sender) if err != nil { return err } @@ -43,9 +43,18 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e // Update user's borrow in store borrow.Amount = borrow.Amount.Sub(payment) - k.SetBorrow(ctx, borrow) - k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, sender) + // Calculate the new Loan-to-Value ratio of Deposit-to-Borrow + deposit, foundDeposit := k.GetDeposit(ctx, sender) + if !foundDeposit { + return types.ErrDepositNotFound + } + newLtv, err := k.CalculateLtv(ctx, deposit, borrow) + if err != nil { + return err + } + + k.UpdateBorrowAndLtvIndex(ctx, borrow, newLtv, prevLtv) // Update total borrowed amount k.DecrementBorrowedCoins(ctx, payment) diff --git a/x/hard/keeper/withdraw.go b/x/hard/keeper/withdraw.go index 61854ce7..33cda8fc 100644 --- a/x/hard/keeper/withdraw.go +++ b/x/hard/keeper/withdraw.go @@ -10,7 +10,7 @@ import ( // 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) + prevLtv, err := k.GetStoreLTV(ctx, depositor) if err != nil { return err } @@ -27,18 +27,17 @@ func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Co 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{} } + proposedDeposit := types.NewDeposit(deposit.Depositor, deposit.Amount.Sub(amount), types.SupplyInterestFactors{}) 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") } @@ -60,9 +59,12 @@ func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Co } deposit.Amount = deposit.Amount.Sub(amount) - k.SetDeposit(ctx, deposit) - k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor) + newLtv, err := k.CalculateLtv(ctx, deposit, borrow) + if err != nil { + return err + } + k.UpdateDepositAndLtvIndex(ctx, deposit, newLtv, prevLtv) // Update total supplied amount k.DecrementBorrowedCoins(ctx, amount)