From 55b73e36eea1fe2bb396f4ee11df92bb2c4cbfb3 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 15 Apr 2020 13:50:14 -0400 Subject: [PATCH] Fix: Check debt limit when adding cdp (#433) * fix: check debt limit when opening cdp * fix: increase debt limit for querier tests --- x/cdp/keeper/cdp.go | 15 +++++++- x/cdp/keeper/cdp_test.go | 13 +++++-- x/cdp/keeper/integration_test.go | 58 +++++++++++++++++++++++++++++ x/cdp/keeper/querier_test.go | 10 +++-- x/cdp/simulation/genesis.go | 6 +-- x/cdp/simulation/operations/msgs.go | 29 +++++++++++++-- 6 files changed, 116 insertions(+), 15 deletions(-) diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 5d729263..9e3611ee 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -27,6 +27,11 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi if err != nil { return err } + + err = k.ValidateDebtLimit(ctx, collateral[0].Denom, principal) + if err != nil { + return err + } err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins()) if err != nil { return err @@ -371,10 +376,18 @@ func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk. return nil } -// ValidateDebtLimit validates that the input debt amount does not exceed the global debt limit +// ValidateDebtLimit validates that the input debt amount does not exceed the global debt limit or the debt limit for that collateral func (k Keeper) ValidateDebtLimit(ctx sdk.Context, collateralDenom string, principal sdk.Coins) sdk.Error { + cp, found := k.GetCollateral(ctx, collateralDenom) + if !found { + return types.ErrCollateralNotSupported(k.codespace, collateralDenom) + } for _, dc := range principal { totalPrincipal := k.GetTotalPrincipal(ctx, collateralDenom, dc.Denom).Add(dc.Amount) + collateralLimit := cp.DebtLimit.AmountOf(dc.Denom) + if totalPrincipal.GT(collateralLimit) { + return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, collateralLimit))) + } globalLimit := k.GetParams(ctx).GlobalDebtLimit.AmountOf(dc.Denom) if totalPrincipal.GT(globalLimit) { return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, globalLimit))) diff --git a/x/cdp/keeper/cdp_test.go b/x/cdp/keeper/cdp_test.go index a76dd8a2..f7ca7982 100644 --- a/x/cdp/keeper/cdp_test.go +++ b/x/cdp/keeper/cdp_test.go @@ -38,7 +38,7 @@ func (suite *CdpTestSuite) SetupTest() { } func (suite *CdpTestSuite) TestAddCdp() { - _, addrs := app.GeneratePrivKeyAddressPairs(1) + _, addrs := app.GeneratePrivKeyAddressPairs(2) ak := suite.app.GetAccountKeeper() acc := ak.NewAccountWithAddress(suite.ctx, addrs[0]) acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000))) @@ -46,14 +46,21 @@ func (suite *CdpTestSuite) TestAddCdp() { err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 26000000))) suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 500000000)), cs(c("usdx", 26000000))) - suite.Error(err) + suite.Error(err) // insufficient balance err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("xusd", 10000000))) suite.Equal(types.CodeDebtNotSupported, err.Result().Code) + + acc2 := ak.NewAccountWithAddress(suite.ctx, addrs[1]) + acc2.SetCoins(cs(c("btc", 500000000000))) + ak.SetAccount(suite.ctx, acc2) + err = suite.keeper.AddCdp(suite.ctx, addrs[1], cs(c("btc", 500000000000)), cs(c("usdx", 500000000001))) + suite.Equal(types.CodeExceedsDebtLimit, err.Result().Code) + ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2)) pk := suite.app.GetPriceFeedKeeper() _ = pk.SetCurrentPrices(ctx, "xrp:usd") err = suite.keeper.AddCdp(ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000))) - suite.Error(err) + suite.Error(err) // no prices in pricefeed _ = pk.SetCurrentPrices(suite.ctx, "xrp:usd") err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000))) diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index 0714b69e..0cfbbf1f 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -159,6 +159,64 @@ func NewCDPGenStateMulti() app.GenesisState { return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } +func NewCDPGenStateHighDebtLimit() app.GenesisState { + cdpGenesis := cdp.GenesisState{ + Params: cdp.Params{ + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 100000000000000), sdk.NewInt64Coin("susd", 100000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, + SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency, + CollateralParams: cdp.CollateralParams{ + { + Denom: "xrp", + LiquidationRatio: sdk.MustNewDecFromStr("2.0"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000), sdk.NewInt64Coin("susd", 50000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(7000000000), + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), + }, + { + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000), sdk.NewInt64Coin("susd", 50000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + LiquidationPenalty: d("0.025"), + AuctionSize: i(10000000), + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), + }, + }, + DebtParams: cdp.DebtParams{ + { + Denom: "usdx", + ReferenceAsset: "usd", + ConversionFactor: i(6), + DebtFloor: i(10000000), + SavingsRate: d("0.95"), + }, + { + Denom: "susd", + ReferenceAsset: "usd", + ConversionFactor: i(6), + DebtFloor: i(10000000), + SavingsRate: d("0.95"), + }, + }, + }, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + PreviousDistributionTime: cdp.DefaultPreviousDistributionTime, + } + return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} +} + func cdps() (cdps cdp.CDPs) { _, addrs := app.GeneratePrivKeyAddressPairs(3) c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(10000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(8000000))), tmtime.Canonical(time.Now())) diff --git a/x/cdp/keeper/querier_test.go b/x/cdp/keeper/querier_test.go index 41b65467..e68afb77 100644 --- a/x/cdp/keeper/querier_test.go +++ b/x/cdp/keeper/querier_test.go @@ -53,7 +53,7 @@ func (suite *QuerierTestSuite) SetupTest() { tApp.InitializeFromGenesisStates( authGS, NewPricefeedGenStateMulti(), - NewCDPGenStateMulti(), + NewCDPGenStateHighDebtLimit(), ) suite.ctx = ctx @@ -93,11 +93,13 @@ func (suite *QuerierTestSuite) SetupTest() { amount = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 500000000, 5000000000) debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 1000000000, 25000000000) } - suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt))))) + err = suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt)))) + suite.NoError(err) c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1)) suite.True(f) cdps[j] = c - aCDP, _ := suite.keeper.LoadAugmentedCDP(suite.ctx, c) + aCDP, err := suite.keeper.LoadAugmentedCDP(suite.ctx, c) + suite.NoError(err) augmentedCDPs[j] = aCDP } @@ -250,7 +252,7 @@ func (suite *QuerierTestSuite) TestQueryParams() { var p types.Params suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p)) - cdpGS := NewCDPGenStateMulti() + cdpGS := NewCDPGenStateHighDebtLimit() gs := types.GenesisState{} types.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs) suite.Equal(gs.Params, p) diff --git a/x/cdp/simulation/genesis.go b/x/cdp/simulation/genesis.go index a82104c9..8f817997 100644 --- a/x/cdp/simulation/genesis.go +++ b/x/cdp/simulation/genesis.go @@ -79,7 +79,7 @@ func randomCdpGenState(selection int) types.GenesisState { DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 20000000000000)), StabilityFee: sdk.MustNewDecFromStr("1.000000004431822130"), LiquidationPenalty: sdk.MustNewDecFromStr("0.075"), - AuctionSize: sdk.NewInt(10000000000), + AuctionSize: sdk.NewInt(100000000000), Prefix: 0x20, MarketID: "xrp:usd", ConversionFactor: sdk.NewInt(6), @@ -90,7 +90,7 @@ func randomCdpGenState(selection int) types.GenesisState { DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000)), StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), - AuctionSize: sdk.NewInt(50000000), + AuctionSize: sdk.NewInt(1000000000), Prefix: 0x21, MarketID: "btc:usd", ConversionFactor: sdk.NewInt(8), @@ -101,7 +101,7 @@ func randomCdpGenState(selection int) types.GenesisState { DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 30000000000000)), StabilityFee: sdk.MustNewDecFromStr("1.000000002293273137"), LiquidationPenalty: sdk.MustNewDecFromStr("0.15"), - AuctionSize: sdk.NewInt(10000000000), + AuctionSize: sdk.NewInt(1000000000000), Prefix: 0x22, MarketID: "bnb:usd", ConversionFactor: sdk.NewInt(8), diff --git a/x/cdp/simulation/operations/msgs.go b/x/cdp/simulation/operations/msgs.go index 29c6a6bf..7600d87b 100644 --- a/x/cdp/simulation/operations/msgs.go +++ b/x/cdp/simulation/operations/msgs.go @@ -52,9 +52,9 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k cdp.Keeper, pfk pricefeed.Keeper) s minCollateralDeposit = ShiftDec(minCollateralDeposit, randCollateralParam.ConversionFactor) // convert to integer and always round up minCollateralDepositRounded := minCollateralDeposit.TruncateInt().Add(sdk.OneInt()) - // if the account has less than the min deposit, return if coins.AmountOf(randCollateralParam.Denom).LT(minCollateralDepositRounded) { - return simulation.NoOpMsg(cdp.ModuleName), nil, nil + // account doesn't have enough funds to open a cdp for the min debt amount + return simulation.NewOperationMsgBasic(cdp.ModuleName, "no-operation", "insufficient funds to open cdp", false, nil), nil, nil } // set the max collateral deposit to the amount of coins in the account maxCollateralDeposit := coins.AmountOf(randCollateralParam.Denom) @@ -65,6 +65,14 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k cdp.Keeper, pfk pricefeed.Keeper) s collateralDepositValue := ShiftDec(sdk.NewDecFromInt(collateralDeposit), randCollateralParam.ConversionFactor.Neg()).Mul(priceShifted) // calculate the max amount of debt that could be drawn for the chosen deposit maxDebtDraw := collateralDepositValue.Quo(randCollateralParam.LiquidationRatio).TruncateInt() + // check that the debt limit hasn't been reached + availableAssetDebt := randCollateralParam.DebtLimit.AmountOf(randDebtParam.Denom).Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, randDebtParam.Denom)) + if availableAssetDebt.LTE(randDebtParam.DebtFloor) { + // debt limit has been reached + return simulation.NewOperationMsgBasic(cdp.ModuleName, "no-operation", "debt limit reached, cannot open cdp", false, nil), nil, nil + } + // ensure that the debt draw does not exceed the debt limit + maxDebtDraw = sdk.MinInt(maxDebtDraw, availableAssetDebt) // randomly select a debt draw amount debtDraw := sdk.NewInt(int64(simulation.RandIntBetween(r, int(randDebtParam.DebtFloor.Int64()), int(maxDebtDraw.Int64())))) msg := cdp.NewMsgCreateCDP(acc.GetAddress(), sdk.NewCoins(sdk.NewCoin(randCollateralParam.Denom, collateralDeposit)), sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, debtDraw))) @@ -114,10 +122,23 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k cdp.Keeper, pfk pricefeed.Keeper) s if shouldDraw(r) { collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.AmountOf(randCollateralParam.Denom)), randCollateralParam.ConversionFactor.Neg()) collateralValue := collateralShifted.Mul(priceShifted) + // given the current collateral value, calculate how much debt we could add while maintaining a valid liquidation ratio debt := (existingCDP.Principal.Add(existingCDP.AccumulatedFees)).AmountOf(randDebtParam.Denom) maxTotalDebt := collateralValue.Quo(randCollateralParam.LiquidationRatio) - maxDebt := maxTotalDebt.Sub(sdk.NewDecFromInt(debt)).TruncateInt().Sub(sdk.OneInt()) - randDrawAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxDebt.Int64())))) + maxDebt := maxTotalDebt.Sub(sdk.NewDecFromInt(debt)).TruncateInt() + if maxDebt.LTE(sdk.OneInt()) { + // debt in cdp is maxed out + return simulation.NewOperationMsgBasic(cdp.ModuleName, "no-operation", "cdp debt maxed out, cannot draw more debt", false, nil), nil, nil + } + // check if the debt limit has been reached + availableAssetDebt := randCollateralParam.DebtLimit.AmountOf(randDebtParam.Denom).Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Denom, randDebtParam.Denom)) + if availableAssetDebt.LTE(sdk.OneInt()) { + // debt limit has been reached + return simulation.NewOperationMsgBasic(cdp.ModuleName, "no-operation", "debt limit reached, cannot draw more debt", false, nil), nil, nil + } + maxDraw := sdk.MinInt(maxDebt, availableAssetDebt) + + randDrawAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxDraw.Int64())))) msg := cdp.NewMsgDrawDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, randDrawAmount))) err := msg.ValidateBasic() if err != nil {