From fe2a131b3146d237d6492ece411015391b4c785a Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Fri, 12 Feb 2021 16:28:05 +0100 Subject: [PATCH] Hard Audit: add minimum borrow USD value (#822) * add module param MinimumBorrowUSDValue * borrow/repay min limit restrictions * add borrow/repay test cases * update tests with new module params * update timelock test with param * update withdraw LTV test * remove unused GetCurrentBorrowUSDValue method * commit to prompt CircleCI run --- x/hard/keeper/borrow.go | 6 ++ x/hard/keeper/borrow_test.go | 45 +++++++++++++++ x/hard/keeper/deposit_test.go | 1 + x/hard/keeper/interest_test.go | 2 + x/hard/keeper/liquidation_test.go | 1 + x/hard/keeper/params.go | 6 ++ x/hard/keeper/repay.go | 78 ++++++++++++++++++++++++-- x/hard/keeper/repay_test.go | 33 +++++++++++ x/hard/keeper/timelock_test.go | 1 + x/hard/keeper/withdraw_test.go | 16 ++++-- x/hard/types/errors.go | 2 + x/hard/types/genesis_test.go | 1 + x/hard/types/params.go | 53 ++++++++++++----- x/hard/types/params_test.go | 9 ++- x/incentive/keeper/integration_test.go | 1 + 15 files changed, 225 insertions(+), 30 deletions(-) diff --git a/x/hard/keeper/borrow.go b/x/hard/keeper/borrow.go index b17f2726..26d4d54a 100644 --- a/x/hard/keeper/borrow.go +++ b/x/hard/keeper/borrow.go @@ -208,6 +208,12 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount } } + // Borrow's updated total USD value must be greater than the minimum global USD borrow limit + totalBorrowUSDValue := proprosedBorrowUSDValue.Add(existingBorrowUSDValue) + if totalBorrowUSDValue.LT(k.GetMinimumBorrowUSDValue(ctx)) { + return sdkerrors.Wrapf(types.ErrBelowMinimumBorrowValue, "the proposed borrow's USD value $%s is below the minimum borrow limit $%s", totalBorrowUSDValue, k.GetMinimumBorrowUSDValue(ctx)) + } + // Validate that the proposed borrow's USD value is within user's borrowable limit if proprosedBorrowUSDValue.GT(totalBorrowableAmount.Sub(existingBorrowUSDValue)) { return sdkerrors.Wrapf(types.ErrInsufficientLoanToValue, "requested borrow %s exceeds the allowable amount as determined by the collateralization ratio", amount) diff --git a/x/hard/keeper/borrow_test.go b/x/hard/keeper/borrow_test.go index 0e2525d0..4217c9f5 100644 --- a/x/hard/keeper/borrow_test.go +++ b/x/hard/keeper/borrow_test.go @@ -245,6 +245,50 @@ func (suite *KeeperTestSuite) TestBorrow() { contains: "fails global asset borrow limit validation", }, }, + { + "invalid: borrowing an individual coin type results in a borrow that's under the minimum USD borrow limit", + args{ + usdxBorrowLimit: sdk.MustNewDecFromStr("20000000"), + priceKAVA: sdk.MustNewDecFromStr("2.00"), + loanToValueKAVA: sdk.MustNewDecFromStr("0.8"), + priceBTCB: sdk.MustNewDecFromStr("0.00"), + loanToValueBTCB: sdk.MustNewDecFromStr("0.01"), + priceBNB: sdk.MustNewDecFromStr("0.00"), + loanToValueBNB: sdk.MustNewDecFromStr("0.01"), + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))), + previousBorrowCoins: sdk.NewCoins(), + borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(5*USDX_CF))), + expectedAccountBalance: sdk.NewCoins(), + expectedModAccountBalance: sdk.NewCoins(), + }, + errArgs{ + expectPass: false, + contains: "below the minimum borrow limit", + }, + }, + { + "invalid: borrowing multiple coins results in a borrow that's under the minimum USD borrow limit", + args{ + usdxBorrowLimit: sdk.MustNewDecFromStr("20000000"), + priceKAVA: sdk.MustNewDecFromStr("2.00"), + loanToValueKAVA: sdk.MustNewDecFromStr("0.8"), + priceBTCB: sdk.MustNewDecFromStr("0.00"), + loanToValueBTCB: sdk.MustNewDecFromStr("0.01"), + priceBNB: sdk.MustNewDecFromStr("0.00"), + loanToValueBNB: sdk.MustNewDecFromStr("0.01"), + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))), + previousBorrowCoins: sdk.NewCoins(), + borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(5*USDX_CF)), sdk.NewCoin("ukava", sdk.NewInt(2*USDX_CF))), + expectedAccountBalance: sdk.NewCoins(), + expectedModAccountBalance: sdk.NewCoins(), + }, + errArgs{ + expectPass: false, + contains: "below the minimum borrow limit", + }, + }, } for _, tc := range testCases { suite.Run(tc.name, func() { @@ -269,6 +313,7 @@ func (suite *KeeperTestSuite) TestBorrow() { 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"), sdk.ZeroDec()), 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"), sdk.ZeroDec()), }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) diff --git a/x/hard/keeper/deposit_test.go b/x/hard/keeper/deposit_test.go index baba8de1..4855e9f0 100644 --- a/x/hard/keeper/deposit_test.go +++ b/x/hard/keeper/deposit_test.go @@ -111,6 +111,7 @@ func (suite *KeeperTestSuite) TestDeposit() { types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "bnb:usd", sdk.NewInt(1000000), 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(1000000000000000), loanToValue), "btcb:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) diff --git a/x/hard/keeper/interest_test.go b/x/hard/keeper/interest_test.go index db4b177c..f6f71912 100644 --- a/x/hard/keeper/interest_test.go +++ b/x/hard/keeper/interest_test.go @@ -796,6 +796,7 @@ func (suite *KeeperTestSuite) TestBorrowInterest() { tc.args.reserveFactor, // Reserve Factor sdk.ZeroDec()), // Keeper Reward Percentage }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) @@ -1209,6 +1210,7 @@ func (suite *KeeperTestSuite) TestSupplyInterest() { tc.args.reserveFactor, // Reserve Factor sdk.ZeroDec()), // Keeper Reward Percentage }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) diff --git a/x/hard/keeper/liquidation_test.go b/x/hard/keeper/liquidation_test.go index 0584434a..b31bd972 100644 --- a/x/hard/keeper/liquidation_test.go +++ b/x/hard/keeper/liquidation_test.go @@ -511,6 +511,7 @@ func (suite *KeeperTestSuite) TestKeeperLiquidation() { reserveFactor, // Reserve Factor tc.args.keeperRewardPercent), // Keeper Reward Percent }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) diff --git a/x/hard/keeper/params.go b/x/hard/keeper/params.go index e57643e8..bfdec301 100644 --- a/x/hard/keeper/params.go +++ b/x/hard/keeper/params.go @@ -28,3 +28,9 @@ func (k Keeper) GetMoneyMarketParam(ctx sdk.Context, denom string) (types.MoneyM } return types.MoneyMarket{}, false } + +// GetMinimumBorrowUSDValue returns the minimum borrow USD value +func (k Keeper) GetMinimumBorrowUSDValue(ctx sdk.Context) sdk.Dec { + params := k.GetParams(ctx) + return params.MinimumBorrowUSDValue +} diff --git a/x/hard/keeper/repay.go b/x/hard/keeper/repay.go index 61db7f20..d191610b 100644 --- a/x/hard/keeper/repay.go +++ b/x/hard/keeper/repay.go @@ -21,7 +21,7 @@ func (k Keeper) Repay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.C k.SyncBorrowInterest(ctx, owner) // Validate that sender holds coins for repayment - err := k.ValidateRepay(ctx, sender, coins) + err := k.ValidateRepay(ctx, sender, owner, coins) if err != nil { return err } @@ -78,14 +78,80 @@ func (k Keeper) Repay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.C } // ValidateRepay validates a requested loan repay -func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { +func (k Keeper) ValidateRepay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.Coins) error { + moneyMarketCache := map[string]types.MoneyMarket{} + assetPriceCache := map[string]sdk.Dec{} + + // Get the total USD value of user's existing borrows + existingBorrowUSDValue := sdk.ZeroDec() + existingBorrow, found := k.GetBorrow(ctx, owner) + if found { + for _, borrowedCoin := range existingBorrow.Amount { + moneyMarket, ok := moneyMarketCache[borrowedCoin.Denom] + if !ok { // Fetch money market and store in local cache + newMoneyMarket, found := k.GetMoneyMarketParam(ctx, borrowedCoin.Denom) + if !found { + return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", borrowedCoin.Denom) + } + moneyMarketCache[borrowedCoin.Denom] = newMoneyMarket + moneyMarket = newMoneyMarket + } + + assetPrice, ok := assetPriceCache[borrowedCoin.Denom] + if !ok { // Fetch current asset price and store in local cache + assetPriceInfo, err := k.pricefeedKeeper.GetCurrentPrice(ctx, moneyMarket.SpotMarketID) + if err != nil { + return sdkerrors.Wrapf(types.ErrPriceNotFound, "no price found for market %s", moneyMarket.SpotMarketID) + } + assetPriceCache[borrowedCoin.Denom] = assetPriceInfo.Price + assetPrice = assetPriceInfo.Price + } + + // Calculate this borrow coin's USD value and add it to the total previous borrowed USD value + coinUSDValue := sdk.NewDecFromInt(borrowedCoin.Amount).Quo(sdk.NewDecFromInt(moneyMarket.ConversionFactor)).Mul(assetPrice) + existingBorrowUSDValue = existingBorrowUSDValue.Add(coinUSDValue) + } + } + senderAcc := k.accountKeeper.GetAccount(ctx, sender) senderCoins := senderAcc.SpendableCoins(ctx.BlockTime()) - - for _, coin := range coins { - if senderCoins.AmountOf(coin.Denom).LT(coin.Amount) { - return sdkerrors.Wrapf(types.ErrInsufficientBalanceForRepay, "account can only repay up to %s%s", senderCoins.AmountOf(coin.Denom), coin.Denom) + repayTotalUSDValue := sdk.ZeroDec() + for _, repayCoin := range coins { + // Check that sender holds enough tokens to make the proposed payment + if senderCoins.AmountOf(repayCoin.Denom).LT(repayCoin.Amount) { + return sdkerrors.Wrapf(types.ErrInsufficientBalanceForRepay, "account can only repay up to %s%s", senderCoins.AmountOf(repayCoin.Denom), repayCoin.Denom) } + + moneyMarket, ok := moneyMarketCache[repayCoin.Denom] + if !ok { // Fetch money market and store in local cache + newMoneyMarket, found := k.GetMoneyMarketParam(ctx, repayCoin.Denom) + if !found { + return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", repayCoin.Denom) + } + moneyMarketCache[repayCoin.Denom] = newMoneyMarket + moneyMarket = newMoneyMarket + } + + // Calculate this coin's USD value and add it to the repay's total USD value + assetPrice, ok := assetPriceCache[repayCoin.Denom] + if !ok { // Fetch current asset price and store in local cache + assetPriceInfo, err := k.pricefeedKeeper.GetCurrentPrice(ctx, moneyMarket.SpotMarketID) + if err != nil { + return sdkerrors.Wrapf(types.ErrPriceNotFound, "no price found for market %s", moneyMarket.SpotMarketID) + } + assetPriceCache[repayCoin.Denom] = assetPriceInfo.Price + assetPrice = assetPriceInfo.Price + } + coinUSDValue := sdk.NewDecFromInt(repayCoin.Amount).Quo(sdk.NewDecFromInt(moneyMarket.ConversionFactor)).Mul(assetPrice) + repayTotalUSDValue = repayTotalUSDValue.Add(coinUSDValue) + } + + // If the proposed repayment would results in a borrowed USD value below the minimum borrow USD value, reject it. + // User can overpay their loan to close it out, but underpaying by such a margin that the USD value is in an + // invalid range is not allowed + proposedBorrowNewUSDValue := existingBorrowUSDValue.Sub(repayTotalUSDValue) + if proposedBorrowNewUSDValue.IsPositive() && proposedBorrowNewUSDValue.LT(k.GetMinimumBorrowUSDValue(ctx)) { + return sdkerrors.Wrapf(types.ErrBelowMinimumBorrowValue, "the proposed borrow's USD value $%s is below the minimum borrow limit $%s", proposedBorrowNewUSDValue, k.GetMinimumBorrowUSDValue(ctx)) } return nil diff --git a/x/hard/keeper/repay_test.go b/x/hard/keeper/repay_test.go index 8062e5c3..de8331b1 100644 --- a/x/hard/keeper/repay_test.go +++ b/x/hard/keeper/repay_test.go @@ -122,6 +122,38 @@ func (suite *KeeperTestSuite) TestRepay() { contains: "account can only repay up to 50000000ukava", }, }, + { + "invalid: repaying a single coin type results in borrow position below the minimum USD value", + args{ + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(50*USDX_CF))), + repayCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(45*USDX_CF))), + }, + errArgs{ + expectPass: false, + expectDelete: false, + contains: "proposed borrow's USD value $5.000000000000000000 is below the minimum borrow limit", + }, + }, + { + "invalid: repaying multiple coin types results in borrow position below the minimum USD value", + args{ + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(50*USDX_CF)), sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))), // (50*$1)+(10*$2) = $70 + repayCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(45*USDX_CF)), sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), // (45*$1)+(8*2) = $61 + }, + errArgs{ + expectPass: false, + expectDelete: false, + contains: "proposed borrow's USD value $9.000000000000000000 is below the minimum borrow limit", + }, + }, } for _, tc := range testCases { @@ -153,6 +185,7 @@ func (suite *KeeperTestSuite) TestRepay() { sdk.MustNewDecFromStr("0.05"), // Reserve Factor sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) diff --git a/x/hard/keeper/timelock_test.go b/x/hard/keeper/timelock_test.go index b45eb6cc..ca63410e 100644 --- a/x/hard/keeper/timelock_test.go +++ b/x/hard/keeper/timelock_test.go @@ -285,6 +285,7 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() { 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"), sdk.ZeroDec()), 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"), sdk.ZeroDec()), }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) diff --git a/x/hard/keeper/withdraw_test.go b/x/hard/keeper/withdraw_test.go index 9a4b63f6..f7a7fe27 100644 --- a/x/hard/keeper/withdraw_test.go +++ b/x/hard/keeper/withdraw_test.go @@ -129,6 +129,7 @@ func (suite *KeeperTestSuite) TestWithdraw() { 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"), sdk.ZeroDec()), types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "bnb:usd", sdk.NewInt(100000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) @@ -212,6 +213,7 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() { initialBorrowerCoins sdk.Coins depositCoins sdk.Coins borrowCoins sdk.Coins + repayCoins sdk.Coins futureTime int64 } @@ -239,8 +241,9 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() { borrower: borrower, initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(100*KAVA_CF))), - depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))), // 10 * 2 = $20 - borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), // 8 * 2 = $16 + depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), // 100 * 2 = $200 + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(80*KAVA_CF))), // 80 * 2 = $160 + repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(60*KAVA_CF))), // 60 * 2 = $120 futureTime: oneMonthInSeconds, }, errArgs{ @@ -280,6 +283,7 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() { reserveFactor, // Reserve Factor sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent }, + sdk.NewDec(10), ), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows, types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves, ) @@ -353,8 +357,8 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() { 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.borrower, tc.args.borrowCoins) + // Repay the initial principal. Over pay the position so the borrow is closed. + err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrower, tc.args.repayCoins) suite.Require().NoError(err) // Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt @@ -362,8 +366,8 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() { 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)))) + // Withdrawing 10% of the coins should succeed + withdrawCoins := sdk.NewCoins(sdk.NewCoin("ukava", tc.args.depositCoins[0].Amount.Quo(sdk.NewInt(10)))) err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins) suite.Require().NoError(err) }) diff --git a/x/hard/types/errors.go b/x/hard/types/errors.go index 65992406..9de2cafb 100644 --- a/x/hard/types/errors.go +++ b/x/hard/types/errors.go @@ -63,4 +63,6 @@ var ( ErrInvalidRepaymentDenom = sdkerrors.Register(ModuleName, 28, "no coins of this type borrowed") // ErrInvalidIndexFactorDenom error for when index factor denom cannot be found ErrInvalidIndexFactorDenom = sdkerrors.Register(ModuleName, 29, "no index factor found for denom") + // ErrBelowMinimumBorrowValue error for when a proposed borrow position is less than the minimum USD value + ErrBelowMinimumBorrowValue = sdkerrors.Register(ModuleName, 30, "invalid proposed borrow value") ) diff --git a/x/hard/types/genesis_test.go b/x/hard/types/genesis_test.go index 402b5c4d..153ba565 100644 --- a/x/hard/types/genesis_test.go +++ b/x/hard/types/genesis_test.go @@ -61,6 +61,7 @@ func (suite *GenesisTestSuite) TestGenesisValidation() { types.MoneyMarkets{ types.NewMoneyMarket("usdx", types.NewBorrowLimit(true, sdk.MustNewDecFromStr("100000000000"), 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"), sdk.ZeroDec()), }, + sdk.MustNewDecFromStr("10"), ), gats: types.GenesisAccumulationTimes{ types.NewGenesisAccumulationTime("usdx", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), sdk.OneDec(), sdk.OneDec()), diff --git a/x/hard/types/params.go b/x/hard/types/params.go index 301347c6..b9fb0279 100644 --- a/x/hard/types/params.go +++ b/x/hard/types/params.go @@ -11,20 +11,23 @@ import ( // Parameter keys and default values var ( - KeyMoneyMarkets = []byte("MoneyMarkets") - DefaultMoneyMarkets = MoneyMarkets{} - GovDenom = cdptypes.DefaultGovDenom - DefaultAccumulationTimes = GenesisAccumulationTimes{} - DefaultTotalSupplied = sdk.Coins{} - DefaultTotalBorrowed = sdk.Coins{} - DefaultTotalReserves = sdk.Coins{} - DefaultDeposits = Deposits{} - DefaultBorrows = Borrows{} + KeyMoneyMarkets = []byte("MoneyMarkets") + KeyMinimumBorrowUSDValue = []byte("MinimumBorrowUSDValue") + DefaultMoneyMarkets = MoneyMarkets{} + DefaultMinimumBorrowUSDValue = sdk.NewDec(10) // $10 USD minimum borrow value + GovDenom = cdptypes.DefaultGovDenom + DefaultAccumulationTimes = GenesisAccumulationTimes{} + DefaultTotalSupplied = sdk.Coins{} + DefaultTotalBorrowed = sdk.Coins{} + DefaultTotalReserves = sdk.Coins{} + DefaultDeposits = Deposits{} + DefaultBorrows = Borrows{} ) // Params governance parameters for hard module type Params struct { - MoneyMarkets MoneyMarkets `json:"money_markets" yaml:"money_markets"` + MoneyMarkets MoneyMarkets `json:"money_markets" yaml:"money_markets"` + MinimumBorrowUSDValue sdk.Dec `json:"minimum_borrow_usd_value" yaml:"minimum_borrow_usd_value"` } // BorrowLimit enforces restrictions on a money market @@ -220,22 +223,24 @@ func (irm InterestRateModel) Equal(irmCompareTo InterestRateModel) bool { type InterestRateModels []InterestRateModel // NewParams returns a new params object -func NewParams(moneyMarkets MoneyMarkets) Params { +func NewParams(moneyMarkets MoneyMarkets, minimumBorrowUSDValue sdk.Dec) Params { return Params{ - MoneyMarkets: moneyMarkets, + MoneyMarkets: moneyMarkets, + MinimumBorrowUSDValue: minimumBorrowUSDValue, } } // DefaultParams returns default params for hard module func DefaultParams() Params { - return NewParams(DefaultMoneyMarkets) + return NewParams(DefaultMoneyMarkets, DefaultMinimumBorrowUSDValue) } // String implements fmt.Stringer func (p Params) String() string { return fmt.Sprintf(`Params: - Money Markets %v`, - p.MoneyMarkets) + Minimum Borrow USD Value: %v + Money Markets: %v`, + p.MinimumBorrowUSDValue, p.MoneyMarkets) } // ParamKeyTable Key declaration for parameters @@ -247,14 +252,32 @@ func ParamKeyTable() params.KeyTable { func (p *Params) ParamSetPairs() params.ParamSetPairs { return params.ParamSetPairs{ params.NewParamSetPair(KeyMoneyMarkets, &p.MoneyMarkets, validateMoneyMarketParams), + params.NewParamSetPair(KeyMinimumBorrowUSDValue, &p.MinimumBorrowUSDValue, validateMinimumBorrowUSDValue), } } // Validate checks that the parameters have valid values. func (p Params) Validate() error { + if err := validateMinimumBorrowUSDValue(p.MinimumBorrowUSDValue); err != nil { + return err + } + return validateMoneyMarketParams(p.MoneyMarkets) } +func validateMinimumBorrowUSDValue(i interface{}) error { + minBorrowVal, ok := i.(sdk.Dec) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if minBorrowVal.IsNegative() { + return fmt.Errorf("Minimum borrow USD value cannot be negative") + } + + return nil +} + func validateMoneyMarketParams(i interface{}) error { mm, ok := i.(MoneyMarkets) if !ok { diff --git a/x/hard/types/params_test.go b/x/hard/types/params_test.go index 4ca160cd..690a410c 100644 --- a/x/hard/types/params_test.go +++ b/x/hard/types/params_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" "github.com/kava-labs/kava/x/hard/types" @@ -15,7 +16,8 @@ type ParamTestSuite struct { func (suite *ParamTestSuite) TestParamValidation() { type args struct { - mms types.MoneyMarkets + minBorrowVal sdk.Dec + mms types.MoneyMarkets } testCases := []struct { name string @@ -26,7 +28,8 @@ func (suite *ParamTestSuite) TestParamValidation() { { name: "default", args: args{ - mms: types.DefaultMoneyMarkets, + minBorrowVal: types.DefaultMinimumBorrowUSDValue, + mms: types.DefaultMoneyMarkets, }, expectPass: true, expectedErr: "", @@ -34,7 +37,7 @@ func (suite *ParamTestSuite) TestParamValidation() { } for _, tc := range testCases { suite.Run(tc.name, func() { - params := types.NewParams(tc.args.mms) + params := types.NewParams(tc.args.mms, tc.args.minBorrowVal) err := params.Validate() if tc.expectPass { suite.NoError(err) diff --git a/x/incentive/keeper/integration_test.go b/x/incentive/keeper/integration_test.go index e6191d55..a1931aad 100644 --- a/x/incentive/keeper/integration_test.go +++ b/x/incentive/keeper/integration_test.go @@ -159,6 +159,7 @@ func NewHardGenStateMulti() app.GenesisState { hard.NewMoneyMarket("btcb", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "btc:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), hard.NewMoneyMarket("xrp", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "xrp:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()), }, + sdk.NewDec(10), ), hard.DefaultAccumulationTimes, hard.DefaultDeposits, hard.DefaultBorrows, hard.DefaultTotalSupplied, hard.DefaultTotalBorrowed, hard.DefaultTotalReserves, )