mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 01:07:28 +00:00 
			
		
		
		
	Hard: withdraws limited by ltv (#747)
* liquidations refactor * validate withdraws above ltv * set mm in initgenesis * add ltv limited withdraw test * address revisions * resolve diff
This commit is contained in:
		
							parent
							
								
									a4bbea1ec4
								
							
						
					
					
						commit
						477b937039
					
				@ -27,6 +27,10 @@ func InitGenesis(ctx sdk.Context, k Keeper, supplyKeeper types.SupplyKeeper, gs
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, mm := range gs.Params.MoneyMarkets {
 | 
			
		||||
		k.SetMoneyMarket(ctx, mm.Denom, mm)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if the module account exists
 | 
			
		||||
	LPModuleAcc := supplyKeeper.GetModuleAccount(ctx, LPAccount)
 | 
			
		||||
	if LPModuleAcc == nil {
 | 
			
		||||
 | 
			
		||||
@ -91,11 +91,35 @@ func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Co
 | 
			
		||||
		return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit found for %s", depositor)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !deposit.Amount.IsAllGTE(coins) { // TODO test that this works how I think it does
 | 
			
		||||
		return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "%s>%s", coins, deposit.Amount)
 | 
			
		||||
	// Get current stored LTV based on stored borrows/deposits
 | 
			
		||||
	prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, coins)
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
@ -122,6 +146,8 @@ func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Co
 | 
			
		||||
	deposit.Amount = deposit.Amount.Sub(coins)
 | 
			
		||||
	k.SetDeposit(ctx, deposit)
 | 
			
		||||
 | 
			
		||||
	k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
package keeper_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@ -267,7 +266,7 @@ func (suite *KeeperTestSuite) TestWithdraw() {
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "invalid withdrawal amount",
 | 
			
		||||
				contains:   "subtraction results in negative borrow amount",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
@ -284,7 +283,7 @@ func (suite *KeeperTestSuite) TestWithdraw() {
 | 
			
		||||
			},
 | 
			
		||||
			errArgs{
 | 
			
		||||
				expectPass: false,
 | 
			
		||||
				contains:   "invalid withdrawal amount",
 | 
			
		||||
				contains:   "subtraction results in negative borrow amount",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
@ -310,10 +309,45 @@ func (suite *KeeperTestSuite) TestWithdraw() {
 | 
			
		||||
				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)
 | 
			
		||||
			tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
 | 
			
		||||
 | 
			
		||||
			// 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
 | 
			
		||||
@ -341,10 +375,181 @@ func (suite *KeeperTestSuite) TestWithdraw() {
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				suite.Require().Error(err)
 | 
			
		||||
				fmt.Printf("%s\n", err.Error())
 | 
			
		||||
				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)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -48,60 +48,25 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
 | 
			
		||||
	if !found {
 | 
			
		||||
		return false, sdkerrors.Wrapf(types.ErrDepositsNotFound, "no deposits found for %s", borrower)
 | 
			
		||||
	}
 | 
			
		||||
	depositDenoms := []string{}
 | 
			
		||||
	for _, depCoin := range deposit.Amount {
 | 
			
		||||
		depositDenoms = append(depositDenoms, depCoin.Denom)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fetch borrow balances and parse coin denoms
 | 
			
		||||
	borrows, found := k.GetBorrow(ctx, borrower)
 | 
			
		||||
	borrow, found := k.GetBorrow(ctx, borrower)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return false, types.ErrBorrowNotFound
 | 
			
		||||
	}
 | 
			
		||||
	borrowDenoms := getDenoms(borrows.Amount)
 | 
			
		||||
 | 
			
		||||
	liqMap := make(map[string]LiqData)
 | 
			
		||||
 | 
			
		||||
	// Load required liquidation data for every deposit/borrow denom
 | 
			
		||||
	denoms := removeDuplicates(borrowDenoms, depositDenoms)
 | 
			
		||||
	for _, denom := range denoms {
 | 
			
		||||
		mm, found := k.GetMoneyMarket(ctx, denom)
 | 
			
		||||
		if !found {
 | 
			
		||||
			return false, sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", denom)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		priceData, err := k.pricefeedKeeper.GetCurrentPrice(ctx, mm.SpotMarketID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		liqMap[denom] = LiqData{priceData.Price, mm.BorrowLimit.LoanToValue, mm.ConversionFactor}
 | 
			
		||||
	isWithinRange, err := k.IsWithinValidLtvRange(ctx, deposit, borrow)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	totalBorrowableUSDAmount := sdk.ZeroDec()
 | 
			
		||||
	totalDepositedUSDAmount := sdk.ZeroDec()
 | 
			
		||||
	for _, depCoin := range deposit.Amount {
 | 
			
		||||
		lData := liqMap[depCoin.Denom]
 | 
			
		||||
		usdValue := sdk.NewDecFromInt(depCoin.Amount).Quo(sdk.NewDecFromInt(lData.conversionFactor)).Mul(lData.price)
 | 
			
		||||
		totalDepositedUSDAmount = totalDepositedUSDAmount.Add(usdValue)
 | 
			
		||||
		borrowableUSDAmountForDeposit := usdValue.Mul(lData.ltv)
 | 
			
		||||
		totalBorrowableUSDAmount = totalBorrowableUSDAmount.Add(borrowableUSDAmountForDeposit)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	totalBorrowedUSDAmount := sdk.ZeroDec()
 | 
			
		||||
	for _, coin := range borrows.Amount {
 | 
			
		||||
		lData := liqMap[coin.Denom]
 | 
			
		||||
		usdValue := sdk.NewDecFromInt(coin.Amount).Quo(sdk.NewDecFromInt(lData.conversionFactor)).Mul(lData.price)
 | 
			
		||||
		totalBorrowedUSDAmount = totalBorrowedUSDAmount.Add(usdValue)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Validate that the proposed borrow's USD value is within user's borrowable limit
 | 
			
		||||
	if totalBorrowedUSDAmount.LTE(totalBorrowableUSDAmount) {
 | 
			
		||||
		return false, sdkerrors.Wrapf(types.ErrBorrowNotLiquidatable, "borrowed %s <= borrowable %s", totalBorrowedUSDAmount, totalBorrowableUSDAmount)
 | 
			
		||||
	if isWithinRange {
 | 
			
		||||
		return false, sdkerrors.Wrapf(types.ErrBorrowNotLiquidatable, "position is within valid LTV range")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Sending coins to auction module with keeper address getting % of the profits
 | 
			
		||||
	err = k.SeizeDeposits(ctx, keeper, liqMap, deposit, borrows.Amount, depositDenoms, borrowDenoms)
 | 
			
		||||
	borrowDenoms := getDenoms(borrow.Amount)
 | 
			
		||||
	depositDenoms := getDenoms(deposit.Amount)
 | 
			
		||||
	err = k.SeizeDeposits(ctx, keeper, deposit, borrow, depositDenoms, borrowDenoms)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
@ -112,7 +77,6 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
 | 
			
		||||
	}
 | 
			
		||||
	k.RemoveFromLtvIndex(ctx, currLtv, borrower)
 | 
			
		||||
 | 
			
		||||
	borrow, _ := k.GetBorrow(ctx, borrower)
 | 
			
		||||
	k.DeleteBorrow(ctx, borrow)
 | 
			
		||||
	k.DeleteDeposit(ctx, deposit)
 | 
			
		||||
 | 
			
		||||
@ -120,8 +84,12 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SeizeDeposits seizes a list of deposits and sends them to auction
 | 
			
		||||
func (k Keeper) SeizeDeposits(ctx sdk.Context, keeper sdk.AccAddress, liqMap map[string]LiqData,
 | 
			
		||||
	deposit types.Deposit, borrowBalances sdk.Coins, dDenoms, bDenoms []string) error {
 | 
			
		||||
func (k Keeper) SeizeDeposits(ctx sdk.Context, keeper sdk.AccAddress, deposit types.Deposit,
 | 
			
		||||
	borrow types.Borrow, dDenoms, bDenoms []string) error {
 | 
			
		||||
	liqMap, err := k.LoadLiquidationData(ctx, deposit, borrow)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Seize % of every deposit and send to the keeper
 | 
			
		||||
	aucDeposits := sdk.Coins{}
 | 
			
		||||
@ -158,7 +126,7 @@ func (k Keeper) SeizeDeposits(ctx sdk.Context, keeper sdk.AccAddress, liqMap map
 | 
			
		||||
 | 
			
		||||
	// Build valuation map to hold borrow coin USD valuations
 | 
			
		||||
	borrowCoinValues := types.NewValuationMap()
 | 
			
		||||
	for _, bCoin := range borrowBalances {
 | 
			
		||||
	for _, bCoin := range borrow.Amount {
 | 
			
		||||
		bData := liqMap[bCoin.Denom]
 | 
			
		||||
		bCoinUsdValue := sdk.NewDecFromInt(bCoin.Amount).Quo(sdk.NewDecFromInt(bData.conversionFactor)).Mul(bData.price)
 | 
			
		||||
		borrowCoinValues.Increment(bCoin.Denom, bCoinUsdValue)
 | 
			
		||||
@ -167,7 +135,7 @@ func (k Keeper) SeizeDeposits(ctx sdk.Context, keeper sdk.AccAddress, liqMap map
 | 
			
		||||
	// Loan-to-Value ratio after sending keeper their reward
 | 
			
		||||
	ltv := borrowCoinValues.Sum().Quo(depositCoinValues.Sum())
 | 
			
		||||
 | 
			
		||||
	err := k.StartAuctions(ctx, deposit.Depositor, borrowBalances, aucDeposits, depositCoinValues, borrowCoinValues, ltv, liqMap)
 | 
			
		||||
	err = k.StartAuctions(ctx, deposit.Depositor, borrow.Amount, aucDeposits, depositCoinValues, borrowCoinValues, ltv, liqMap)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@ -276,68 +244,36 @@ func (k Keeper) StartAuctions(ctx sdk.Context, borrower sdk.AccAddress, borrows,
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	// Fetch deposits and parse coin denoms
 | 
			
		||||
	deposit, found := k.GetDeposit(ctx, addr)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return sdk.ZeroDec(), false, nil
 | 
			
		||||
// IsWithinValidLtvRange compares a borrow and deposit to see if it's within a valid LTV range at current prices
 | 
			
		||||
func (k Keeper) IsWithinValidLtvRange(ctx sdk.Context, deposit types.Deposit, borrow types.Borrow) (bool, error) {
 | 
			
		||||
	liqMap, err := k.LoadLiquidationData(ctx, deposit, borrow)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	depositDenoms := []string{}
 | 
			
		||||
 | 
			
		||||
	totalBorrowableUSDAmount := sdk.ZeroDec()
 | 
			
		||||
	totalDepositedUSDAmount := sdk.ZeroDec()
 | 
			
		||||
	for _, depCoin := range deposit.Amount {
 | 
			
		||||
		depositDenoms = append(depositDenoms, depCoin.Denom)
 | 
			
		||||
		lData := liqMap[depCoin.Denom]
 | 
			
		||||
		usdValue := sdk.NewDecFromInt(depCoin.Amount).Quo(sdk.NewDecFromInt(lData.conversionFactor)).Mul(lData.price)
 | 
			
		||||
		totalDepositedUSDAmount = totalDepositedUSDAmount.Add(usdValue)
 | 
			
		||||
		borrowableUSDAmountForDeposit := usdValue.Mul(lData.ltv)
 | 
			
		||||
		totalBorrowableUSDAmount = totalBorrowableUSDAmount.Add(borrowableUSDAmountForDeposit)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fetch borrow balances and parse coin denoms
 | 
			
		||||
	borrows, found := k.GetBorrow(ctx, addr)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return sdk.ZeroDec(), false, nil
 | 
			
		||||
	}
 | 
			
		||||
	borrowDenoms := getDenoms(borrows.Amount)
 | 
			
		||||
 | 
			
		||||
	liqMap := make(map[string]LiqData)
 | 
			
		||||
 | 
			
		||||
	// Load required liquidation data for every deposit/borrow denom
 | 
			
		||||
	denoms := removeDuplicates(borrowDenoms, depositDenoms)
 | 
			
		||||
	for _, denom := range denoms {
 | 
			
		||||
		mm, found := k.GetMoneyMarket(ctx, denom)
 | 
			
		||||
		if !found {
 | 
			
		||||
			return sdk.ZeroDec(), false, sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", denom)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		priceData, err := k.pricefeedKeeper.GetCurrentPrice(ctx, mm.SpotMarketID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return sdk.ZeroDec(), false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		liqMap[denom] = LiqData{priceData.Price, mm.BorrowLimit.LoanToValue, mm.ConversionFactor}
 | 
			
		||||
	totalBorrowedUSDAmount := sdk.ZeroDec()
 | 
			
		||||
	for _, coin := range borrow.Amount {
 | 
			
		||||
		lData := liqMap[coin.Denom]
 | 
			
		||||
		usdValue := sdk.NewDecFromInt(coin.Amount).Quo(sdk.NewDecFromInt(lData.conversionFactor)).Mul(lData.price)
 | 
			
		||||
		totalBorrowedUSDAmount = totalBorrowedUSDAmount.Add(usdValue)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Build valuation map to hold deposit coin USD valuations
 | 
			
		||||
	depositCoinValues := types.NewValuationMap()
 | 
			
		||||
	for _, depCoin := range deposit.Amount {
 | 
			
		||||
		dData := liqMap[depCoin.Denom]
 | 
			
		||||
		dCoinUsdValue := sdk.NewDecFromInt(depCoin.Amount).Quo(sdk.NewDecFromInt(dData.conversionFactor)).Mul(dData.price)
 | 
			
		||||
		depositCoinValues.Increment(depCoin.Denom, dCoinUsdValue)
 | 
			
		||||
	// Check if the user's has borrowed more than they're allowed to
 | 
			
		||||
	if totalBorrowedUSDAmount.GT(totalBorrowableUSDAmount) {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Build valuation map to hold borrow coin USD valuations
 | 
			
		||||
	borrowCoinValues := types.NewValuationMap()
 | 
			
		||||
	for _, bCoin := range borrows.Amount {
 | 
			
		||||
		bData := liqMap[bCoin.Denom]
 | 
			
		||||
		bCoinUsdValue := sdk.NewDecFromInt(bCoin.Amount).Quo(sdk.NewDecFromInt(bData.conversionFactor)).Mul(bData.price)
 | 
			
		||||
		borrowCoinValues.Increment(bCoin.Denom, bCoinUsdValue)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// User doesn't have any deposits, catch divide by 0 error
 | 
			
		||||
	sumDeposits := depositCoinValues.Sum()
 | 
			
		||||
	if sumDeposits.Equal(sdk.ZeroDec()) {
 | 
			
		||||
		return sdk.ZeroDec(), false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Loan-to-Value ratio
 | 
			
		||||
	return borrowCoinValues.Sum().Quo(sumDeposits), true, nil
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateItemInLtvIndex updates the key a borrower's address is stored under in the LTV index
 | 
			
		||||
@ -357,6 +293,86 @@ func (k Keeper) UpdateItemInLtvIndex(ctx sdk.Context, prevLtv sdk.Dec,
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	// Fetch deposits and parse coin denoms
 | 
			
		||||
	deposit, found := k.GetDeposit(ctx, addr)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return sdk.ZeroDec(), false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fetch borrow balances and parse coin denoms
 | 
			
		||||
	borrow, found := k.GetBorrow(ctx, addr)
 | 
			
		||||
	if !found {
 | 
			
		||||
		return sdk.ZeroDec(), false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return k.CalculateLtv(ctx, deposit, borrow)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	// Load required liquidation data for every deposit/borrow denom
 | 
			
		||||
	liqMap, err := k.LoadLiquidationData(ctx, deposit, borrow)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sdk.ZeroDec(), false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Build valuation map to hold deposit coin USD valuations
 | 
			
		||||
	depositCoinValues := types.NewValuationMap()
 | 
			
		||||
	for _, depCoin := range deposit.Amount {
 | 
			
		||||
		dData := liqMap[depCoin.Denom]
 | 
			
		||||
		dCoinUsdValue := sdk.NewDecFromInt(depCoin.Amount).Quo(sdk.NewDecFromInt(dData.conversionFactor)).Mul(dData.price)
 | 
			
		||||
		depositCoinValues.Increment(depCoin.Denom, dCoinUsdValue)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Build valuation map to hold borrow coin USD valuations
 | 
			
		||||
	borrowCoinValues := types.NewValuationMap()
 | 
			
		||||
	for _, bCoin := range borrow.Amount {
 | 
			
		||||
		bData := liqMap[bCoin.Denom]
 | 
			
		||||
		bCoinUsdValue := sdk.NewDecFromInt(bCoin.Amount).Quo(sdk.NewDecFromInt(bData.conversionFactor)).Mul(bData.price)
 | 
			
		||||
		borrowCoinValues.Increment(bCoin.Denom, bCoinUsdValue)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// User doesn't have any deposits, catch divide by 0 error
 | 
			
		||||
	sumDeposits := depositCoinValues.Sum()
 | 
			
		||||
	if sumDeposits.Equal(sdk.ZeroDec()) {
 | 
			
		||||
		return sdk.ZeroDec(), false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Loan-to-Value ratio
 | 
			
		||||
	return borrowCoinValues.Sum().Quo(sumDeposits), true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadLiquidationData returns liquidation data, deposit, borrow
 | 
			
		||||
func (k Keeper) LoadLiquidationData(ctx sdk.Context, deposit types.Deposit, borrow types.Borrow) (map[string]LiqData, error) {
 | 
			
		||||
	liqMap := make(map[string]LiqData)
 | 
			
		||||
 | 
			
		||||
	borrowDenoms := getDenoms(borrow.Amount)
 | 
			
		||||
	depositDenoms := getDenoms(deposit.Amount)
 | 
			
		||||
	denoms := removeDuplicates(borrowDenoms, depositDenoms)
 | 
			
		||||
 | 
			
		||||
	// Load required liquidation data for every deposit/borrow denom
 | 
			
		||||
	for _, denom := range denoms {
 | 
			
		||||
		mm, found := k.GetMoneyMarket(ctx, denom)
 | 
			
		||||
		if !found {
 | 
			
		||||
			return liqMap, sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", denom)
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		priceData, err := k.pricefeedKeeper.GetCurrentPrice(ctx, mm.SpotMarketID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return liqMap, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		liqMap[denom] = LiqData{priceData.Price, mm.BorrowLimit.LoanToValue, mm.ConversionFactor}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return liqMap, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDenoms(coins sdk.Coins) []string {
 | 
			
		||||
	denoms := []string{}
 | 
			
		||||
	for _, coin := range coins {
 | 
			
		||||
 | 
			
		||||
@ -412,7 +412,7 @@ func (suite *KeeperTestSuite) TestFullIndexLiquidation() {
 | 
			
		||||
							MaxEndTime:      endTime,
 | 
			
		||||
						},
 | 
			
		||||
						CorrespondingDebt: sdk.NewInt64Coin("debt", 0),
 | 
			
		||||
						MaxBid:            sdk.NewInt64Coin("ukava", 8013492), // TODO: why isn't this 8004766
 | 
			
		||||
						MaxBid:            sdk.NewInt64Coin("ukava", 8013492),
 | 
			
		||||
						LotReturns:        lotReturns,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
@ -449,7 +449,7 @@ func (suite *KeeperTestSuite) TestFullIndexLiquidation() {
 | 
			
		||||
							MaxEndTime:      endTime,
 | 
			
		||||
						},
 | 
			
		||||
						CorrespondingDebt: sdk.NewInt64Coin("debt", 0),
 | 
			
		||||
						MaxBid:            sdk.NewInt64Coin("ukava", 8014873), // TODO: Why isn't this 8013492
 | 
			
		||||
						MaxBid:            sdk.NewInt64Coin("ukava", 8014873),
 | 
			
		||||
						LotReturns:        otherBorrower3LotReturns,
 | 
			
		||||
					},
 | 
			
		||||
					auctypes.CollateralAuction{
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user