diff --git a/app/app.go b/app/app.go index d91ddeb1..9b25f93f 100644 --- a/app/app.go +++ b/app/app.go @@ -106,6 +106,7 @@ var ( harvest.LPAccount: {supply.Minter, supply.Burner}, harvest.DelegatorAccount: {supply.Minter, supply.Burner}, harvest.ModuleAccountName: {supply.Minter, supply.Burner}, + harvest.LiquidatorAccount: {supply.Minter, supply.Burner}, } // module accounts that are allowed to receive tokens @@ -386,7 +387,9 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio app.accountKeeper, app.supplyKeeper, &stakingKeeper, - app.pricefeedKeeper) + app.pricefeedKeeper, + app.auctionKeeper, + ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks diff --git a/x/harvest/alias.go b/x/harvest/alias.go index 539460be..15e87a2c 100644 --- a/x/harvest/alias.go +++ b/x/harvest/alias.go @@ -31,6 +31,7 @@ const ( EventTypeHarvestWithdrawal = types.EventTypeHarvestWithdrawal LP = types.LP LPAccount = types.LPAccount + LiquidatorAccount = types.LiquidatorAccount Large = types.Large Medium = types.Medium ModuleAccountName = types.ModuleAccountName diff --git a/x/harvest/client/cli/tx.go b/x/harvest/client/cli/tx.go index 340d0bb3..d87f5718 100644 --- a/x/harvest/client/cli/tx.go +++ b/x/harvest/client/cli/tx.go @@ -34,6 +34,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { getCmdWithdraw(cdc), getCmdClaimReward(cdc), getCmdBorrow(cdc), + getCmdLiquidate(cdc), getCmdRepay(cdc), )...) @@ -173,3 +174,31 @@ func getCmdRepay(cdc *codec.Codec) *cobra.Command { }, } } + +func getCmdLiquidate(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "liquidate [borrower-addr]", + Short: "liquidate a borrower that's over their loan-to-value ratio", + Long: strings.TrimSpace(`liquidate a borrower that's over their loan-to-value ratio`), + Args: cobra.ExactArgs(1), + Example: fmt.Sprintf( + `%s tx %s borrow kava1hgcfsuwc889wtdmt8pjy7qffua9dd2tralu64j --from `, version.ClientName, types.ModuleName, + ), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + + borrower, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + msg := types.NewMsgLiquidate(cliCtx.GetFromAddress(), borrower) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} diff --git a/x/harvest/genesis.go b/x/harvest/genesis.go index 4a9cd04d..baf01ca5 100644 --- a/x/harvest/genesis.go +++ b/x/harvest/genesis.go @@ -45,6 +45,12 @@ func InitGenesis(ctx sdk.Context, k Keeper, supplyKeeper types.SupplyKeeper, gs panic(fmt.Sprintf("%s module account has not been set", DepositModuleAccount)) } + // check if the module account exists + LiquidatorModuleAcc := supplyKeeper.GetModuleAccount(ctx, LiquidatorAccount) + if LiquidatorModuleAcc == nil { + panic(fmt.Sprintf("%s module account has not been set", LiquidatorAccount)) + } + } // ExportGenesis export genesis state for harvest module diff --git a/x/harvest/keeper/borrow_test.go b/x/harvest/keeper/borrow_test.go index 45b03935..aa537500 100644 --- a/x/harvest/keeper/borrow_test.go +++ b/x/harvest/keeper/borrow_test.go @@ -275,12 +275,12 @@ func (suite *KeeperTestSuite) TestBorrow() { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(true, tc.args.usdxBorrowLimit, sdk.MustNewDecFromStr("1")), "usdx:usd", sdk.NewInt(USDX_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("busd", types.NewBorrowLimit(false, sdk.NewDec(100000000*BUSD_CF), sdk.MustNewDecFromStr("1")), "busd:usd", sdk.NewInt(BUSD_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), tc.args.loanToValueKAVA), "kava:usd", sdk.NewInt(KAVA_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("btcb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BTCB_CF), tc.args.loanToValueBTCB), "btcb:usd", sdk.NewInt(BTCB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), tc.args.loanToValueBNB), "bnb:usd", sdk.NewInt(BNB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("xyz", types.NewBorrowLimit(false, sdk.NewDec(1), tc.args.loanToValueBNB), "xyz:usd", sdk.NewInt(1), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.NewMoneyMarket("usdx", types.NewBorrowLimit(true, tc.args.usdxBorrowLimit, sdk.MustNewDecFromStr("1")), "usdx:usd", sdk.NewInt(USDX_CF), 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("busd", types.NewBorrowLimit(false, sdk.NewDec(100000000*BUSD_CF), sdk.MustNewDecFromStr("1")), "busd:usd", sdk.NewInt(BUSD_CF), sdk.NewInt(BUSD_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(100000000*KAVA_CF), tc.args.loanToValueKAVA), "kava:usd", sdk.NewInt(KAVA_CF), 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("btcb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BTCB_CF), tc.args.loanToValueBTCB), "btcb:usd", sdk.NewInt(BTCB_CF), sdk.NewInt(BTCB_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(100000000*BNB_CF), tc.args.loanToValueBNB), "bnb:usd", sdk.NewInt(BNB_CF), 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()), + types.NewMoneyMarket("xyz", types.NewBorrowLimit(false, sdk.NewDec(1), tc.args.loanToValueBNB), "xyz:usd", sdk.NewInt(1), sdk.NewInt(1), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) diff --git a/x/harvest/keeper/claim_test.go b/x/harvest/keeper/claim_test.go index 3c8f2b06..0bb9464c 100644 --- a/x/harvest/keeper/claim_test.go +++ b/x/harvest/keeper/claim_test.go @@ -265,8 +265,8 @@ func (suite *KeeperTestSuite) TestClaim() { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.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.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) diff --git a/x/harvest/keeper/deposit_test.go b/x/harvest/keeper/deposit_test.go index 99d8895b..823a579f 100644 --- a/x/harvest/keeper/deposit_test.go +++ b/x/harvest/keeper/deposit_test.go @@ -108,8 +108,8 @@ func (suite *KeeperTestSuite) TestDeposit() { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.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.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) @@ -251,8 +251,8 @@ func (suite *KeeperTestSuite) TestWithdraw() { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.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.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) diff --git a/x/harvest/keeper/interest_test.go b/x/harvest/keeper/interest_test.go index b922ba24..2551bbee 100644 --- a/x/harvest/keeper/interest_test.go +++ b/x/harvest/keeper/interest_test.go @@ -659,10 +659,12 @@ func (suite *KeeperTestSuite) TestInterest() { types.MoneyMarkets{ types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit - "kava:usd", // Market ID - sdk.NewInt(KAVA_CF), // Conversion Factor - tc.args.interestRateModel, - tc.args.reserveFactor), // Reserve Factor + "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.DefaultPreviousBlockTime, types.DefaultDistributionTimes) diff --git a/x/harvest/keeper/keeper.go b/x/harvest/keeper/keeper.go index b2e29948..2662f6ff 100644 --- a/x/harvest/keeper/keeper.go +++ b/x/harvest/keeper/keeper.go @@ -20,12 +20,13 @@ type Keeper struct { supplyKeeper types.SupplyKeeper stakingKeeper types.StakingKeeper pricefeedKeeper types.PricefeedKeeper + auctionKeeper types.AuctionKeeper } // NewKeeper creates a new keeper func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, ak types.AccountKeeper, sk types.SupplyKeeper, stk types.StakingKeeper, - pfk types.PricefeedKeeper) Keeper { + pfk types.PricefeedKeeper, auk types.AuctionKeeper) Keeper { if !paramstore.HasKeyTable() { paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) } @@ -38,6 +39,7 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, supplyKeeper: sk, stakingKeeper: stk, pricefeedKeeper: pfk, + auctionKeeper: auk, } } diff --git a/x/harvest/keeper/keeper_test.go b/x/harvest/keeper/keeper_test.go index 9c42a43a..ed6200a1 100644 --- a/x/harvest/keeper/keeper_test.go +++ b/x/harvest/keeper/keeper_test.go @@ -15,6 +15,7 @@ import ( tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/app" + aucKeeper "github.com/kava-labs/kava/x/auction/keeper" "github.com/kava-labs/kava/x/harvest/keeper" "github.com/kava-labs/kava/x/harvest/types" ) @@ -22,10 +23,11 @@ import ( // Test suite used for all keeper tests type KeeperTestSuite struct { suite.Suite - keeper keeper.Keeper - app app.TestApp - ctx sdk.Context - addrs []sdk.AccAddress + keeper keeper.Keeper + auctionKeeper aucKeeper.Keeper + app app.TestApp + ctx sdk.Context + addrs []sdk.AccAddress } // The default state used by each test @@ -158,7 +160,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteInterestRateModel() { denom := "test" model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")) borrowLimit := types.NewBorrowLimit(false, sdk.MustNewDecFromStr("0.2"), sdk.MustNewDecFromStr("0.5")) - moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), model, sdk.MustNewDecFromStr("0.05")) + moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), model, sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()) _, f := suite.keeper.GetMoneyMarket(suite.ctx, denom) suite.Require().False(f) @@ -184,7 +186,7 @@ func (suite *KeeperTestSuite) TestIterateInterestRateModels() { denom := testDenom + strconv.Itoa(i) model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")) borrowLimit := types.NewBorrowLimit(false, sdk.MustNewDecFromStr("0.2"), sdk.MustNewDecFromStr("0.5")) - moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), model, sdk.MustNewDecFromStr("0.05")) + moneyMarket := types.NewMoneyMarket(denom, borrowLimit, denom+":usd", sdk.NewInt(1000000), sdk.NewInt(KAVA_CF*1000), model, sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()) // Store money market in the module's store suite.Require().NotPanics(func() { suite.keeper.SetMoneyMarket(suite.ctx, denom, moneyMarket) }) diff --git a/x/harvest/keeper/liquidation.go b/x/harvest/keeper/liquidation.go new file mode 100644 index 00000000..2ef5f8b9 --- /dev/null +++ b/x/harvest/keeper/liquidation.go @@ -0,0 +1,277 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/kava-labs/kava/x/harvest/types" +) + +// LiqData holds liquidation-related data +type LiqData struct { + price sdk.Dec + ltv sdk.Dec + conversionFactor sdk.Int +} + +// AttemptIndexLiquidations attempts to liquidate the lowest LTV borrows +func (k Keeper) AttemptIndexLiquidations(ctx sdk.Context) error { + // use moneyMarketCache := map[string]types.MoneyMarket{} + + // Iterate over index + // Get borrower's address + // Use borrower's address to fetch borrow object + // Calculate outstanding interest and add to borrow balances + // Use current asset prices from pricefeed to calculate current LTV for each asset + // If LTV of any asset is over the max, liquidate it by + // Sending coins to auction module + // (?) Removing borrow from the store + // (?) Removing borrow LTV from LTV index + + return nil +} + +// AttemptKeeperLiquidation enables a keeper to liquidate an individual borrower's position +func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress, borrower sdk.AccAddress) error { + // Fetch deposits and parse coin denoms + deposits := k.GetDepositsByUser(ctx, borrower) + depositDenoms := []string{} + for _, deposit := range deposits { + depositDenoms = append(depositDenoms, deposit.Amount.Denom) + } + + // Fetch borrow balances and parse coin denoms + borrowBalances := k.GetBorrowBalance(ctx, borrower) + borrowDenoms := getDenoms(borrowBalances) + + 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 sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", denom) + } + + priceData, err := k.pricefeedKeeper.GetCurrentPrice(ctx, mm.SpotMarketID) + if err != nil { + return err + } + + liqMap[denom] = LiqData{priceData.Price, mm.BorrowLimit.LoanToValue, mm.ConversionFactor} + } + + totalBorrowableUSDAmount := sdk.ZeroDec() + totalDepositedUSDAmount := sdk.ZeroDec() + for _, deposit := range deposits { + lData := liqMap[deposit.Amount.Denom] + usdValue := sdk.NewDecFromInt(deposit.Amount.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 borrowBalances { + 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 sdkerrors.Wrapf(types.ErrBorrowNotLiquidatable, "borrowed %s <= borrowable %s", totalBorrowedUSDAmount, totalBorrowableUSDAmount) + } + + // Sending coins to auction module with keeper address getting % of the profits + borrow, _ := k.GetBorrow(ctx, borrower) + err := k.SeizeDeposits(ctx, keeper, liqMap, deposits, borrowBalances, depositDenoms, borrowDenoms) + if err != nil { + return err + } + + k.DeleteBorrow(ctx, borrow) + + for _, oldDeposit := range deposits { + k.DeleteDeposit(ctx, oldDeposit) + } + + return nil +} + +// 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, + deposits []types.Deposit, borrowBalances sdk.Coins, dDenoms, bDenoms []string) error { + + // Seize % of every deposit and send to the keeper + aucDeposits := sdk.Coins{} + for _, deposit := range deposits { + denom := deposit.Amount.Denom + amount := deposit.Amount.Amount + mm, _ := k.GetMoneyMarket(ctx, denom) + + keeperReward := mm.KeeperRewardPercentage.MulInt(amount).TruncateInt() + if keeperReward.GT(sdk.ZeroInt()) { + // Send keeper their reward + keeperCoin := sdk.NewCoin(denom, keeperReward) + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, keeper, sdk.NewCoins(keeperCoin)) + if err != nil { + return err + } + amount = amount.Sub(keeperReward) + } + // Add remaining deposit coin to aucDeposits + aucDeposits = aucDeposits.Add(sdk.NewCoin(denom, amount)) + } + + // Build valuation map to hold deposit coin USD valuations + depositCoinValues := types.NewValuationMap() + for _, deposit := range aucDeposits { + dData := liqMap[deposit.Denom] + dCoinUsdValue := sdk.NewDecFromInt(deposit.Amount).Quo(sdk.NewDecFromInt(dData.conversionFactor)).Mul(dData.price) + depositCoinValues.Increment(deposit.Denom, dCoinUsdValue) + } + + // Build valuation map to hold borrow coin USD valuations + borrowCoinValues := types.NewValuationMap() + for _, bCoin := range borrowBalances { + bData := liqMap[bCoin.Denom] + bCoinUsdValue := sdk.NewDecFromInt(bCoin.Amount).Quo(sdk.NewDecFromInt(bData.conversionFactor)).Mul(bData.price) + borrowCoinValues.Increment(bCoin.Denom, bCoinUsdValue) + } + + // Loan-to-Value ratio after sending keeper their reward + ltv := borrowCoinValues.Sum().Quo(depositCoinValues.Sum()) + + err := k.StartAuctions(ctx, deposits[0].Depositor, borrowBalances, aucDeposits, depositCoinValues, borrowCoinValues, ltv, liqMap) + if err != nil { + return err + } + + return nil +} + +// StartAuctions attempts to start auctions for seized assets +func (k Keeper) StartAuctions(ctx sdk.Context, borrower sdk.AccAddress, borrows, deposits sdk.Coins, + depositCoinValues, borrowCoinValues types.ValuationMap, ltv sdk.Dec, liqMap map[string]LiqData) error { + // Sort keys to ensure deterministic behavior + bKeys := borrowCoinValues.GetSortedKeys() + dKeys := depositCoinValues.GetSortedKeys() + + // Set up auction constants + returnAddrs := []sdk.AccAddress{borrower} + weights := []sdk.Int{sdk.NewInt(100)} + debt := sdk.NewCoin("debt", sdk.ZeroInt()) + + for _, bKey := range bKeys { + bValue := borrowCoinValues.Get(bKey) + maxLotSize := bValue.Quo(ltv) + + for _, dKey := range dKeys { + dValue := depositCoinValues.Get(dKey) + if maxLotSize.Equal(sdk.ZeroDec()) { + break // exit out of the loop if we have cleared the full amount + } + + if dValue.GTE(maxLotSize) { // We can start an auction for the whole borrow amount + bid := sdk.NewCoin(bKey, borrows.AmountOf(bKey)) + lotSize := maxLotSize.MulInt(liqMap[dKey].conversionFactor).Quo(liqMap[dKey].price) + lot := sdk.NewCoin(dKey, lotSize.TruncateInt()) + // Sanity check that we can deliver coins to the liquidator account + if deposits.AmountOf(dKey).LT(lot.Amount) { + return types.ErrInsufficientCoins + } + + // Start auction: bid = full borrow amount, lot = maxLotSize + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, types.LiquidatorAccount, sdk.NewCoins(lot)) + if err != nil { + return err + } + _, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorAccount, lot, bid, returnAddrs, weights, debt) + if err != nil { + return err + } + + // Update USD valuation maps + borrowCoinValues.SetZero(bKey) + depositCoinValues.Decrement(dKey, maxLotSize) + // Update deposits, borrows + borrows = borrows.Sub(sdk.NewCoins(bid)) + deposits = deposits.Sub(sdk.NewCoins(lot)) + // Update max lot size + maxLotSize = sdk.ZeroDec() + } else { // We can only start an auction for the partial borrow amount + maxBid := dValue.Mul(ltv) + bidSize := maxBid.MulInt(liqMap[bKey].conversionFactor).Quo(liqMap[bKey].price) + bid := sdk.NewCoin(bKey, bidSize.TruncateInt()) + lot := sdk.NewCoin(dKey, deposits.AmountOf(dKey)) + + if bid.Amount.Equal(sdk.ZeroInt()) || lot.Amount.Equal(sdk.ZeroInt()) { + continue + } + + // Sanity check that we can deliver coins to the liquidator account + if deposits.AmountOf(dKey).LT(lot.Amount) { + return types.ErrInsufficientCoins + } + + // Start auction: bid = maxBid, lot = whole deposit amount + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, types.LiquidatorAccount, sdk.NewCoins(lot)) + if err != nil { + return err + } + _, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorAccount, lot, bid, returnAddrs, weights, debt) + if err != nil { + return err + } + + // Update variables to account for partial auction + borrowCoinValues.Decrement(bKey, maxBid) + depositCoinValues.SetZero(dKey) + // Update deposits, borrows + borrows = borrows.Sub(sdk.NewCoins(bid)) + deposits = deposits.Sub(sdk.NewCoins(lot)) + // Update max lot size + maxLotSize = borrowCoinValues.Get(bKey).Quo(ltv) + } + } + } + + // Send any remaining deposit back to the original borrower + for _, dKey := range dKeys { + remaining := deposits.AmountOf(dKey) + if remaining.GT(sdk.ZeroInt()) { + returnCoin := sdk.NewCoins(sdk.NewCoin(dKey, remaining)) + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, borrower, returnCoin) + if err != nil { + return err + } + } + } + + return nil +} + +func getDenoms(coins sdk.Coins) []string { + denoms := []string{} + for _, coin := range coins { + denoms = append(denoms, coin.Denom) + } + return denoms +} + +func removeDuplicates(one []string, two []string) []string { + check := make(map[string]int) + fullList := append(one, two...) + + res := []string{} + for _, val := range fullList { + check[val] = 1 + } + + for key := range check { + res = append(res, key) + } + return res +} diff --git a/x/harvest/keeper/liquidation_test.go b/x/harvest/keeper/liquidation_test.go new file mode 100644 index 00000000..8c464a71 --- /dev/null +++ b/x/harvest/keeper/liquidation_test.go @@ -0,0 +1,681 @@ +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" + auctypes "github.com/kava-labs/kava/x/auction/types" + "github.com/kava-labs/kava/x/harvest" + "github.com/kava-labs/kava/x/harvest/types" + "github.com/kava-labs/kava/x/pricefeed" +) + +func (suite *KeeperTestSuite) TestKeeperLiquidation() { + type args struct { + borrower sdk.AccAddress + keeper sdk.AccAddress + keeperRewardPercent sdk.Dec + initialModuleCoins sdk.Coins + initialBorrowerCoins sdk.Coins + initialKeeperCoins sdk.Coins + depositCoins []sdk.Coin + borrowCoins sdk.Coins + liquidateAfter int64 + auctionSize sdk.Int + expectedKeeperCoins sdk.Coins // coins keeper address should have after successfully liquidating position + expectedBorrowerCoins sdk.Coins // additional coins (if any) the borrower address should have after successfully liquidating position + expectedAuctions auctypes.Auctions // the auctions we should expect to find have been started + } + + 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"))) + + // Set up auction constants + layout := "2006-01-02T15:04:05.000Z" + endTimeStr := "9000-01-01T00:00:00.000Z" + endTime, _ := time.Parse(layout, endTimeStr) + + lotReturns, _ := auctypes.NewWeightedAddresses([]sdk.AccAddress{borrower}, []sdk.Int{sdk.NewInt(100)}) + + testCases := []liqTest{ + { + "valid: keeper liquidates borrow", + args{ + borrower: borrower, + keeper: keeper, + keeperRewardPercent: sdk.MustNewDecFromStr("0.05"), + 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.Coin{sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), + liquidateAfter: oneMonthInSeconds, + auctionSize: sdk.NewInt(KAVA_CF * 1000), + expectedKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100.5*KAVA_CF))), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(98000001))), // initial - deposit + borrow + liquidation leftovers + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 9499999), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 8004766), + LotReturns: lotReturns, + }, + }, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid: single deposit, multiple borrows", + args{ + borrower: borrower, + keeper: keeper, + keeperRewardPercent: sdk.MustNewDecFromStr("0.05"), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(1000*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(1000*BTCB_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.Coin{sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))}, // $100 * 0.8 = $80 borrowable + borrowCoins: sdk.NewCoins(sdk.NewCoin("usdc", sdk.NewInt(20*KAVA_CF)), sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(2*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(0.2*BTCB_CF))), // $20+$20+$20 = $80 borrowed + liquidateAfter: oneMonthInSeconds, + auctionSize: sdk.NewInt(KAVA_CF * 1000), + expectedKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(102.5*KAVA_CF))), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("usdc", sdk.NewInt(20*KAVA_CF)), sdk.NewCoin("ukava", sdk.NewInt(60*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(2*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(0.2*BTCB_CF))), // initial - deposit + borrow + liquidation leftovers + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 11874422), + Bidder: nil, + Bid: sdk.NewInt64Coin("bnb", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("bnb", 200003287), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 2, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 11874245), + Bidder: nil, + Bid: sdk.NewInt64Coin("btc", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("btc", 20000032), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 3, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 11875155), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 10000782), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 4, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 11876178), + Bidder: nil, + Bid: sdk.NewInt64Coin("usdc", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("usdc", 20003283), + LotReturns: lotReturns, + }, + }, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid: multiple deposits, single borrow", + args{ + borrower: borrower, + keeper: keeper, + keeperRewardPercent: sdk.MustNewDecFromStr("0.05"), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(100*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(100*BTCB_CF))), + initialKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(10*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(1*BTCB_CF))}, // $100 + $100 + $100 = $300 * 0.8 = $240 borrowable // $100 * 0.8 = $80 borrowable + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(120*KAVA_CF))), // $240 borrowed + liquidateAfter: oneMonthInSeconds, + auctionSize: sdk.NewInt(KAVA_CF * 1000), + expectedKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(102.5*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(0.5*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(0.05*BTCB_CF))), // 5% of each seized coin + initial balances + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(170.000001*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(90*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(99*BTCB_CF))), + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("bnb", 950000000), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 40037377), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 2, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("btc", 95000000), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 40037377), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 3, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("ukava", 47499999), + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 40037379), + LotReturns: lotReturns, + }, + }, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid: mutliple stablecoin deposits, multiple variable coin borrows", + // Auctions: total lot value = $285 ($300 of deposits - $15 keeper reward), total max bid value = $270 + args{ + borrower: borrower, + keeper: keeper, + keeperRewardPercent: sdk.MustNewDecFromStr("0.05"), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(1000*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(1000*BTCB_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdt", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(100*KAVA_CF))), + initialKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("usdc", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdt", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(100*KAVA_CF))}, // $100 + $100 + $100 = $300 * 0.9 = $270 borrowable + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(35*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(10*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(1*BTCB_CF))), // $270 borrowed + liquidateAfter: oneMonthInSeconds, + auctionSize: sdk.NewInt(KAVA_CF * 1000), + expectedKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(5*KAVA_CF)), sdk.NewCoin("usdt", sdk.NewInt(5*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(5*KAVA_CF))), // 5% of each seized coin + initial balances + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(135*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(10*BNB_CF)), sdk.NewCoin("btc", sdk.NewInt(1*BTCB_CF)), sdk.NewCoin("usdx", sdk.NewInt(0.000001*KAVA_CF))), + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("usdc", 95000000), // $95.00 + Bidder: nil, + Bid: sdk.NewInt64Coin("bnb", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("bnb", 900097134), // $90.00 + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 2, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("usdt", 10552835), // $10.55 + Bidder: nil, + Bid: sdk.NewInt64Coin("bnb", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("bnb", 99985020), // $10.00 + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 3, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("usdt", 84447165), // $84.45 + Bidder: nil, + Bid: sdk.NewInt64Coin("btc", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("btc", 80011211), // $80.01 + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 4, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("usdx", 21097866), // $21.10 + Bidder: nil, + Bid: sdk.NewInt64Coin("btc", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("btc", 19989610), // $19.99 + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 5, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("usdx", 73902133), //$73.90 + Bidder: nil, + Bid: sdk.NewInt64Coin("ukava", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("ukava", 35010052), // $70.02 + LotReturns: lotReturns, + }, + }, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid: multiple stablecoin deposits, multiple stablecoin borrows", + args{ + borrower: borrower, + keeper: keeper, + keeperRewardPercent: sdk.MustNewDecFromStr("0.05"), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdt", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("dai", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(1000*KAVA_CF))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdt", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("dai", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(1000*KAVA_CF))), + initialKeeperCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdt", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("dai", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(1000*KAVA_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("dai", sdk.NewInt(350*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(200*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("usdt", sdk.NewInt(250*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(245*KAVA_CF))), + liquidateAfter: oneMonthInSeconds, + auctionSize: sdk.NewInt(KAVA_CF * 100000), + expectedKeeperCoins: sdk.NewCoins(sdk.NewCoin("dai", sdk.NewInt(1017.50*KAVA_CF)), sdk.NewCoin("usdt", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(1010*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*KAVA_CF))), + expectedBorrowerCoins: sdk.NewCoins(sdk.NewCoin("dai", sdk.NewInt(650*KAVA_CF)), sdk.NewCoin("usdc", sdk.NewInt(800000001)), sdk.NewCoin("usdt", sdk.NewInt(1250*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1245*KAVA_CF))), + expectedAuctions: auctypes.Auctions{ + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 1, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("dai", 263894126), + Bidder: nil, + Bid: sdk.NewInt64Coin("usdt", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("usdt", 250507897), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 2, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("dai", 68605874), + Bidder: nil, + Bid: sdk.NewInt64Coin("usdx", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("usdx", 65125788), + LotReturns: lotReturns, + }, + auctypes.CollateralAuction{ + BaseAuction: auctypes.BaseAuction{ + ID: 3, + Initiator: "harvest_liquidator", + Lot: sdk.NewInt64Coin("usdc", 189999999), + Bidder: nil, + Bid: sdk.NewInt64Coin("usdx", 0), + HasReceivedBids: false, + EndTime: endTime, + MaxEndTime: endTime, + }, + CorrespondingDebt: sdk.NewInt64Coin("debt", 0), + MaxBid: sdk.NewInt64Coin("usdx", 180362106), + LotReturns: lotReturns, + }, + }, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "invalid: borrow not liquidatable", + args{ + borrower: borrower, + keeper: keeper, + keeperRewardPercent: sdk.MustNewDecFromStr("0.05"), + 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.Coin{sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))}, // Deposit 20 KAVA + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(5*KAVA_CF))), // Borrow 5 KAVA + liquidateAfter: oneMonthInSeconds, + auctionSize: sdk.NewInt(KAVA_CF * 1000), + expectedKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100.5*KAVA_CF))), + expectedBorrowerCoins: sdk.NewCoins(), + expectedAuctions: auctypes.Auctions{}, + }, + errArgs{ + expectPass: false, + contains: "borrow not liquidatable", + }, + }, + } + + 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, "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.NewDistributionSchedule(true, "usdc", 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, "usdt", 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, "dai", 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, "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.NewDistributionSchedule(true, "btc", 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("usdx", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "usdx:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + tc.args.auctionSize, // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + tc.args.keeperRewardPercent), // Keeper Reward Percent + types.NewMoneyMarket("usdt", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "usdt:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + tc.args.auctionSize, // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + tc.args.keeperRewardPercent), // Keeper Reward Percent + types.NewMoneyMarket("usdc", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "usdc:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + tc.args.auctionSize, // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + tc.args.keeperRewardPercent), // Keeper Reward Percent + types.NewMoneyMarket("dai", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.9")), // Borrow Limit + "dai:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + tc.args.auctionSize, // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + tc.args.keeperRewardPercent), // Keeper Reward Percent + types.NewMoneyMarket("ukava", + types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit + "kava:usd", // Market ID + sdk.NewInt(KAVA_CF), // Conversion Factor + tc.args.auctionSize, // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + tc.args.keeperRewardPercent), // Keeper Reward Percent + 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 + tc.args.auctionSize, // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + tc.args.keeperRewardPercent), // Keeper Reward Percent + types.NewMoneyMarket("btc", + types.NewBorrowLimit(false, sdk.NewDec(100000000*BTCB_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit + "btc:usd", // Market ID + sdk.NewInt(BTCB_CF), // Conversion Factor + tc.args.auctionSize, // Auction Size + model, // Interest Rate Model + reserveFactor, // Reserve Factor + tc.args.keeperRewardPercent), // Keeper Reward Percent + }, + ), 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: "usdt:usd", BaseAsset: "usdt", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "usdc:usd", BaseAsset: "usdc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "dai:usd", BaseAsset: "dai", 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}, + {MarketID: "btc:usd", BaseAsset: "btc", 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: "usdt:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "usdc:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(100 * time.Hour), + }, + { + MarketID: "dai: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), + }, + { + MarketID: "btc:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("100.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.GetHarvestKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + suite.auctionKeeper = auctionKeeper + + var err error + + // Run begin blocker to set up state + harvest.BeginBlocker(suite.ctx, suite.keeper) + + // Deposit coins + for _, coin := range tc.args.depositCoins { + err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, coin) + suite.Require().NoError(err) + } + + // Borrow coins + err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins) + suite.Require().NoError(err) + + // Set up liquidation chain context and run begin blocker + runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.liquidateAfter), 0) + liqCtx := suite.ctx.WithBlockTime(runAtTime) + harvest.BeginBlocker(liqCtx, suite.keeper) + + // Check borrow exists before liquidation + _, foundBorrowBefore := suite.keeper.GetBorrow(liqCtx, tc.args.borrower) + suite.Require().True(foundBorrowBefore) + // Check that the user's deposits exist before liquidation + for _, coin := range tc.args.depositCoins { + _, foundDepositBefore := suite.keeper.GetDeposit(liqCtx, tc.args.borrower, coin.Denom) + suite.Require().True(foundDepositBefore) + } + + // Attempt to liquidate + err = suite.keeper.AttemptKeeperLiquidation(liqCtx, tc.args.keeper, tc.args.borrower) + if tc.errArgs.expectPass { + suite.Require().NoError(err) + + // Check borrow does not exist after liquidation + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, tc.args.borrower) + suite.Require().False(foundBorrowAfter) + // Check deposits do not exist after liquidation + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, tc.args.borrower, coin.Denom) + suite.Require().False(foundDepositAfter) + } + + // Check that the keeper's balance increased by reward % of all the borrowed coins + accKeeper := suite.getAccountAtCtx(tc.args.keeper, liqCtx) + suite.Require().Equal(tc.args.expectedKeeperCoins, accKeeper.GetCoins()) + + // Check that borrower's balance contains the expected coins + accBorrower := suite.getAccountAtCtx(tc.args.borrower, liqCtx) + suite.Require().Equal(tc.args.expectedBorrowerCoins, accBorrower.GetCoins()) + + // Check that the expected auctions have been created + auctions := suite.auctionKeeper.GetAllAuctions(liqCtx) + suite.Require().True(len(auctions) > 0) + suite.Require().Equal(tc.args.expectedAuctions, auctions) + } else { + suite.Require().Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) + + // Check that the user's borrow exists + _, foundBorrowAfter := suite.keeper.GetBorrow(liqCtx, tc.args.borrower) + suite.Require().True(foundBorrowAfter) + // Check that the user's deposits exist + for _, coin := range tc.args.depositCoins { + _, foundDepositAfter := suite.keeper.GetDeposit(liqCtx, tc.args.borrower, coin.Denom) + suite.Require().True(foundDepositAfter) + } + + // Check that no auctions have been created + auctions := suite.auctionKeeper.GetAllAuctions(liqCtx) + suite.Require().True(len(auctions) == 0) + } + }) + } +} diff --git a/x/harvest/keeper/repay_test.go b/x/harvest/keeper/repay_test.go index 1fce8a9d..b16f5c1b 100644 --- a/x/harvest/keeper/repay_test.go +++ b/x/harvest/keeper/repay_test.go @@ -37,6 +37,8 @@ func (suite *KeeperTestSuite) TestRepay() { errArgs errArgs } + model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")) + testCases := []borrowTest{ { "valid: partial repay", @@ -124,8 +126,22 @@ func (suite *KeeperTestSuite) TestRepay() { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("1")), "usdx:usd", sdk.NewInt(USDX_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("0.8")), "kava:usd", sdk.NewInt(KAVA_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.NewMoneyMarket("usdx", + types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("1")), // Borrow Limit + "usdx:usd", // Market ID + sdk.NewInt(USDX_CF), // Conversion Factor + sdk.NewInt(1000*USDX_CF), // Auction Size + model, // Interest Rate Model + sdk.MustNewDecFromStr("0.05"), // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent + 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(1000*KAVA_CF), // Auction Size + model, // Interest Rate Model + sdk.MustNewDecFromStr("0.05"), // Reserve Factor + sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) diff --git a/x/harvest/keeper/rewards_test.go b/x/harvest/keeper/rewards_test.go index b2eacea3..c5ad8087 100644 --- a/x/harvest/keeper/rewards_test.go +++ b/x/harvest/keeper/rewards_test.go @@ -75,8 +75,8 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.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()), }, ), tc.args.previousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) @@ -443,8 +443,8 @@ func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.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.DefaultPreviousBlockTime, diff --git a/x/harvest/keeper/timelock_test.go b/x/harvest/keeper/timelock_test.go index 8f8d3889..09d42927 100644 --- a/x/harvest/keeper/timelock_test.go +++ b/x/harvest/keeper/timelock_test.go @@ -291,8 +291,8 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() { ), }, types.MoneyMarkets{ - types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), - types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.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.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) diff --git a/x/harvest/types/codec.go b/x/harvest/types/codec.go index 035bd0a4..8858491d 100644 --- a/x/harvest/types/codec.go +++ b/x/harvest/types/codec.go @@ -18,6 +18,7 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgDeposit{}, "harvest/MsgDeposit", nil) cdc.RegisterConcrete(MsgWithdraw{}, "harvest/MsgWithdraw", nil) cdc.RegisterConcrete(MsgBorrow{}, "harvest/MsgBorrow", nil) + cdc.RegisterConcrete(MsgLiquidate{}, "harvest/MsgLiquidate", nil) cdc.RegisterConcrete(MsgRepay{}, "harvest/MsgRepay", nil) cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil) } diff --git a/x/harvest/types/errors.go b/x/harvest/types/errors.go index f6edf94a..eb1f1e2e 100644 --- a/x/harvest/types/errors.go +++ b/x/harvest/types/errors.go @@ -60,5 +60,9 @@ var ( // ErrPreviousAccrualTimeNotFound error for no previous accrual time found in store ErrPreviousAccrualTimeNotFound = sdkerrors.Register(ModuleName, 27, "no previous accrual time found") // ErrInsufficientBalanceForRepay error for when requested repay exceeds user's balance - ErrInsufficientBalanceForRepay = sdkerrors.Register(ModuleName, 29, "insufficient balance") + ErrInsufficientBalanceForRepay = sdkerrors.Register(ModuleName, 28, "insufficient balance") + // ErrBorrowNotLiquidatable error for when a borrow is within valid LTV and cannot be liquidated + ErrBorrowNotLiquidatable = sdkerrors.Register(ModuleName, 29, "borrow not liquidatable") + // ErrInsufficientCoins error for when there are not enough coins for the operation + ErrInsufficientCoins = sdkerrors.Register(ModuleName, 30, "unrecoverable state - insufficient coins") ) diff --git a/x/harvest/types/events.go b/x/harvest/types/events.go index 6dcac400..c568d56f 100644 --- a/x/harvest/types/events.go +++ b/x/harvest/types/events.go @@ -9,12 +9,14 @@ const ( EventTypeHarvestWithdrawal = "harvest_withdrawal" EventTypeClaimHarvestReward = "claim_harvest_reward" EventTypeHarvestBorrow = "harvest_borrow" + EventTypeDepositLiquidation = "harvest_liquidation" EventTypeHarvestRepay = "harvest_repay" AttributeValueCategory = ModuleName AttributeKeyBlockHeight = "block_height" AttributeKeyRewardsDistribution = "rewards_distributed" AttributeKeyDeposit = "deposit" AttributeKeyDepositDenom = "deposit_denom" + AttributeKeyDepositCoins = "deposit_coins" AttributeKeyDepositor = "depositor" AttributeKeyClaimType = "claim_type" AttributeKeyClaimHolder = "claim_holder" diff --git a/x/harvest/types/expected_keepers.go b/x/harvest/types/expected_keepers.go index 7b4d66e5..b19e4475 100644 --- a/x/harvest/types/expected_keepers.go +++ b/x/harvest/types/expected_keepers.go @@ -40,3 +40,8 @@ type StakingKeeper interface { type PricefeedKeeper interface { GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, error) } + +// AuctionKeeper expected interface for the auction keeper (noalias) +type AuctionKeeper interface { + StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, error) +} diff --git a/x/harvest/types/keys.go b/x/harvest/types/keys.go index 334eff24..05dd0b19 100644 --- a/x/harvest/types/keys.go +++ b/x/harvest/types/keys.go @@ -14,6 +14,9 @@ const ( // DelegatorAccount delegator distribution module account DelegatorAccount = "harvest_delegator_distribution" + // LiquidatorAccount module account for liquidator + LiquidatorAccount = "harvest_liquidator" + // ModuleAccountName name of module account used to hold deposits ModuleAccountName = "harvest" diff --git a/x/harvest/types/liquidation.go b/x/harvest/types/liquidation.go new file mode 100644 index 00000000..81398eb9 --- /dev/null +++ b/x/harvest/types/liquidation.go @@ -0,0 +1,70 @@ +package types + +import ( + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ValuationMap holds the USD value of various coin types +type ValuationMap struct { + Usd map[string]sdk.Dec +} + +// NewValuationMap returns a new instance of ValuationMap +func NewValuationMap() ValuationMap { + return ValuationMap{ + Usd: make(map[string]sdk.Dec), + } +} + +// Get returns the USD value for a specific denom +func (m ValuationMap) Get(denom string) sdk.Dec { + return m.Usd[denom] +} + +// SetZero sets the USD value for a specific denom to 0 +func (m ValuationMap) SetZero(denom string) { + m.Usd[denom] = sdk.ZeroDec() +} + +// Increment increments the USD value of a denom +func (m ValuationMap) Increment(denom string, amount sdk.Dec) { + _, ok := m.Usd[denom] + if !ok { + m.Usd[denom] = amount + return + } + m.Usd[denom] = m.Usd[denom].Add(amount) +} + +// Decrement decrements the USD value of a denom +func (m ValuationMap) Decrement(denom string, amount sdk.Dec) { + _, ok := m.Usd[denom] + if !ok { + m.Usd[denom] = amount + return + } + m.Usd[denom] = m.Usd[denom].Sub(amount) +} + +// Sum returns the total USD value of all coins in the map +func (m ValuationMap) Sum() sdk.Dec { + sum := sdk.ZeroDec() + for _, v := range m.Usd { + sum = sum.Add(v) + } + return sum +} + +// GetSortedKeys returns an array of the map's keys in alphabetical order +func (m ValuationMap) GetSortedKeys() []string { + keys := make([]string, len(m.Usd)) + i := 0 + for k := range m.Usd { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys +} diff --git a/x/harvest/types/msg.go b/x/harvest/types/msg.go index cad95154..76e89ce0 100644 --- a/x/harvest/types/msg.go +++ b/x/harvest/types/msg.go @@ -308,3 +308,53 @@ func (msg MsgRepay) String() string { Amount: %s `, msg.Sender, msg.Amount) } + +// MsgLiquidate attempts to liquidate a borrower's borrow +type MsgLiquidate struct { + Keeper sdk.AccAddress `json:"keeper" yaml:"keeper"` + Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"` +} + +// NewMsgLiquidate returns a new MsgLiquidate +func NewMsgLiquidate(keeper, borrower sdk.AccAddress) MsgLiquidate { + return MsgLiquidate{ + Keeper: keeper, + Borrower: borrower, + } +} + +// Route return the message type used for routing the message. +func (msg MsgLiquidate) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgLiquidate) Type() string { return "liquidate" } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgLiquidate) ValidateBasic() error { + if msg.Keeper.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "keeper address cannot be empty") + } + if msg.Borrower.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "borrower address cannot be empty") + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgLiquidate) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgLiquidate) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Keeper} +} + +// String implements the Stringer interface +func (msg MsgLiquidate) String() string { + return fmt.Sprintf(`Liquidate Message: + Keeper: %s + Borrower: %s +`, msg.Keeper, msg.Borrower) +} diff --git a/x/harvest/types/params.go b/x/harvest/types/params.go index 920de402..e0ed1795 100644 --- a/x/harvest/types/params.go +++ b/x/harvest/types/params.go @@ -267,24 +267,28 @@ func (bl BorrowLimit) Equal(blCompareTo BorrowLimit) bool { // MoneyMarket is a money market for an individual asset type MoneyMarket struct { - Denom string `json:"denom" yaml:"denom"` - BorrowLimit BorrowLimit `json:"borrow_limit" yaml:"borrow_limit"` - SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"` - ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` - InterestRateModel InterestRateModel `json:"interest_rate_model" yaml:"interest_rate_model"` - ReserveFactor sdk.Dec `json:"reserve_factor" yaml:"reserve_factor"` + Denom string `json:"denom" yaml:"denom"` + BorrowLimit BorrowLimit `json:"borrow_limit" yaml:"borrow_limit"` + SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"` + ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` + InterestRateModel InterestRateModel `json:"interest_rate_model" yaml:"interest_rate_model"` + ReserveFactor sdk.Dec `json:"reserve_factor" yaml:"reserve_factor"` + AuctionSize sdk.Int `json:"auction_size" yaml:"auction_size"` + KeeperRewardPercentage sdk.Dec `json:"keeper_reward_percentage" yaml:"keeper_reward_percentages"` } // NewMoneyMarket returns a new MoneyMarket -func NewMoneyMarket(denom string, borrowLimit BorrowLimit, spotMarketID string, - conversionFactor sdk.Int, interestRateModel InterestRateModel, reserveFactor sdk.Dec) MoneyMarket { +func NewMoneyMarket(denom string, borrowLimit BorrowLimit, spotMarketID string, conversionFactor, + auctionSize sdk.Int, interestRateModel InterestRateModel, reserveFactor, keeperRewardPercentage sdk.Dec) MoneyMarket { return MoneyMarket{ - Denom: denom, - BorrowLimit: borrowLimit, - SpotMarketID: spotMarketID, - ConversionFactor: conversionFactor, - InterestRateModel: interestRateModel, - ReserveFactor: reserveFactor, + Denom: denom, + BorrowLimit: borrowLimit, + SpotMarketID: spotMarketID, + ConversionFactor: conversionFactor, + AuctionSize: auctionSize, + InterestRateModel: interestRateModel, + ReserveFactor: reserveFactor, + KeeperRewardPercentage: keeperRewardPercentage, } } @@ -305,6 +309,15 @@ func (mm MoneyMarket) Validate() error { if mm.ReserveFactor.IsNegative() || mm.ReserveFactor.GT(sdk.OneDec()) { return fmt.Errorf("Reserve factor must be between 0.0-1.0") } + + if !mm.AuctionSize.IsPositive() { + return fmt.Errorf("Auction size must be a positive integer") + } + + if mm.KeeperRewardPercentage.IsNegative() || mm.KeeperRewardPercentage.GT(sdk.OneDec()) { + return fmt.Errorf("Keeper reward percentage must be between 0.0-1.0") + } + return nil } @@ -328,6 +341,12 @@ func (mm MoneyMarket) Equal(mmCompareTo MoneyMarket) bool { if !mm.ReserveFactor.Equal(mmCompareTo.ReserveFactor) { return false } + if !mm.AuctionSize.Equal(mmCompareTo.AuctionSize) { + return false + } + if !mm.KeeperRewardPercentage.Equal(mmCompareTo.KeeperRewardPercentage) { + return false + } return true } @@ -424,7 +443,8 @@ func (p Params) String() string { Active: %t Liquidity Provider Distribution Schedules %s Delegator Distribution Schedule %s - Money Markets %v`, p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules, p.MoneyMarkets) + Money Markets %v`, + p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules, p.MoneyMarkets) } // ParamKeyTable Key declaration for parameters diff --git a/x/harvest/types/params_test.go b/x/harvest/types/params_test.go index c96a72a9..f2219c17 100644 --- a/x/harvest/types/params_test.go +++ b/x/harvest/types/params_test.go @@ -22,6 +22,7 @@ func (suite *ParamTestSuite) TestParamValidation() { gds types.DistributionSchedules dds types.DelegatorDistributionSchedules mms types.MoneyMarkets + kpr sdk.Dec active bool } testCases := []struct { @@ -52,6 +53,7 @@ func (suite *ParamTestSuite) TestParamValidation() { ), }, mms: types.DefaultMoneyMarkets, + kpr: sdk.MustNewDecFromStr("0.05"), active: true, }, expectPass: true, @@ -69,6 +71,7 @@ func (suite *ParamTestSuite) TestParamValidation() { ), }, mms: types.DefaultMoneyMarkets, + kpr: sdk.MustNewDecFromStr("0.05"), active: true, }, expectPass: false,