Fix: Check debt limit when adding cdp (#433)

* fix: check debt limit when opening cdp

* fix: increase debt limit for querier tests
This commit is contained in:
Kevin Davis 2020-04-15 13:50:14 -04:00 committed by GitHub
parent 45e40fe357
commit 55b73e36ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 116 additions and 15 deletions

View File

@ -27,6 +27,11 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
if err != nil { if err != nil {
return err return err
} }
err = k.ValidateDebtLimit(ctx, collateral[0].Denom, principal)
if err != nil {
return err
}
err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins()) err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins())
if err != nil { if err != nil {
return err return err
@ -371,10 +376,18 @@ func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.
return nil 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 { 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 { for _, dc := range principal {
totalPrincipal := k.GetTotalPrincipal(ctx, collateralDenom, dc.Denom).Add(dc.Amount) 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) globalLimit := k.GetParams(ctx).GlobalDebtLimit.AmountOf(dc.Denom)
if totalPrincipal.GT(globalLimit) { if totalPrincipal.GT(globalLimit) {
return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, globalLimit))) return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, globalLimit)))

View File

@ -38,7 +38,7 @@ func (suite *CdpTestSuite) SetupTest() {
} }
func (suite *CdpTestSuite) TestAddCdp() { func (suite *CdpTestSuite) TestAddCdp() {
_, addrs := app.GeneratePrivKeyAddressPairs(1) _, addrs := app.GeneratePrivKeyAddressPairs(2)
ak := suite.app.GetAccountKeeper() ak := suite.app.GetAccountKeeper()
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0]) acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000))) 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))) err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 26000000)))
suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 500000000)), cs(c("usdx", 26000000))) 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))) err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("xusd", 10000000)))
suite.Equal(types.CodeDebtNotSupported, err.Result().Code) 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)) ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
pk := suite.app.GetPriceFeedKeeper() pk := suite.app.GetPriceFeedKeeper()
_ = pk.SetCurrentPrices(ctx, "xrp:usd") _ = pk.SetCurrentPrices(ctx, "xrp:usd")
err = suite.keeper.AddCdp(ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000))) 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") _ = pk.SetCurrentPrices(suite.ctx, "xrp:usd")
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000))) err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000)))

View File

@ -159,6 +159,64 @@ func NewCDPGenStateMulti() app.GenesisState {
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} 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) { func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3) _, 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())) 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()))

View File

@ -53,7 +53,7 @@ func (suite *QuerierTestSuite) SetupTest() {
tApp.InitializeFromGenesisStates( tApp.InitializeFromGenesisStates(
authGS, authGS,
NewPricefeedGenStateMulti(), NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(), NewCDPGenStateHighDebtLimit(),
) )
suite.ctx = ctx suite.ctx = ctx
@ -93,11 +93,13 @@ func (suite *QuerierTestSuite) SetupTest() {
amount = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 500000000, 5000000000) amount = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 500000000, 5000000000)
debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 1000000000, 25000000000) 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)) c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f) suite.True(f)
cdps[j] = c cdps[j] = c
aCDP, _ := suite.keeper.LoadAugmentedCDP(suite.ctx, c) aCDP, err := suite.keeper.LoadAugmentedCDP(suite.ctx, c)
suite.NoError(err)
augmentedCDPs[j] = aCDP augmentedCDPs[j] = aCDP
} }
@ -250,7 +252,7 @@ func (suite *QuerierTestSuite) TestQueryParams() {
var p types.Params var p types.Params
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p)) suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
cdpGS := NewCDPGenStateMulti() cdpGS := NewCDPGenStateHighDebtLimit()
gs := types.GenesisState{} gs := types.GenesisState{}
types.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs) types.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs)
suite.Equal(gs.Params, p) suite.Equal(gs.Params, p)

View File

@ -79,7 +79,7 @@ func randomCdpGenState(selection int) types.GenesisState {
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 20000000000000)), DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 20000000000000)),
StabilityFee: sdk.MustNewDecFromStr("1.000000004431822130"), StabilityFee: sdk.MustNewDecFromStr("1.000000004431822130"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.075"), LiquidationPenalty: sdk.MustNewDecFromStr("0.075"),
AuctionSize: sdk.NewInt(10000000000), AuctionSize: sdk.NewInt(100000000000),
Prefix: 0x20, Prefix: 0x20,
MarketID: "xrp:usd", MarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
@ -90,7 +90,7 @@ func randomCdpGenState(selection int) types.GenesisState {
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000)), DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 50000000000000)),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000), AuctionSize: sdk.NewInt(1000000000),
Prefix: 0x21, Prefix: 0x21,
MarketID: "btc:usd", MarketID: "btc:usd",
ConversionFactor: sdk.NewInt(8), ConversionFactor: sdk.NewInt(8),
@ -101,7 +101,7 @@ func randomCdpGenState(selection int) types.GenesisState {
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 30000000000000)), DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 30000000000000)),
StabilityFee: sdk.MustNewDecFromStr("1.000000002293273137"), StabilityFee: sdk.MustNewDecFromStr("1.000000002293273137"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.15"), LiquidationPenalty: sdk.MustNewDecFromStr("0.15"),
AuctionSize: sdk.NewInt(10000000000), AuctionSize: sdk.NewInt(1000000000000),
Prefix: 0x22, Prefix: 0x22,
MarketID: "bnb:usd", MarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), ConversionFactor: sdk.NewInt(8),

View File

@ -52,9 +52,9 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k cdp.Keeper, pfk pricefeed.Keeper) s
minCollateralDeposit = ShiftDec(minCollateralDeposit, randCollateralParam.ConversionFactor) minCollateralDeposit = ShiftDec(minCollateralDeposit, randCollateralParam.ConversionFactor)
// convert to integer and always round up // convert to integer and always round up
minCollateralDepositRounded := minCollateralDeposit.TruncateInt().Add(sdk.OneInt()) minCollateralDepositRounded := minCollateralDeposit.TruncateInt().Add(sdk.OneInt())
// if the account has less than the min deposit, return
if coins.AmountOf(randCollateralParam.Denom).LT(minCollateralDepositRounded) { 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 // set the max collateral deposit to the amount of coins in the account
maxCollateralDeposit := coins.AmountOf(randCollateralParam.Denom) 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) collateralDepositValue := ShiftDec(sdk.NewDecFromInt(collateralDeposit), randCollateralParam.ConversionFactor.Neg()).Mul(priceShifted)
// calculate the max amount of debt that could be drawn for the chosen deposit // calculate the max amount of debt that could be drawn for the chosen deposit
maxDebtDraw := collateralDepositValue.Quo(randCollateralParam.LiquidationRatio).TruncateInt() 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 // randomly select a debt draw amount
debtDraw := sdk.NewInt(int64(simulation.RandIntBetween(r, int(randDebtParam.DebtFloor.Int64()), int(maxDebtDraw.Int64())))) 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))) 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) { if shouldDraw(r) {
collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.AmountOf(randCollateralParam.Denom)), randCollateralParam.ConversionFactor.Neg()) collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.AmountOf(randCollateralParam.Denom)), randCollateralParam.ConversionFactor.Neg())
collateralValue := collateralShifted.Mul(priceShifted) 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) debt := (existingCDP.Principal.Add(existingCDP.AccumulatedFees)).AmountOf(randDebtParam.Denom)
maxTotalDebt := collateralValue.Quo(randCollateralParam.LiquidationRatio) maxTotalDebt := collateralValue.Quo(randCollateralParam.LiquidationRatio)
maxDebt := maxTotalDebt.Sub(sdk.NewDecFromInt(debt)).TruncateInt().Sub(sdk.OneInt()) maxDebt := maxTotalDebt.Sub(sdk.NewDecFromInt(debt)).TruncateInt()
randDrawAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxDebt.Int64())))) 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))) msg := cdp.NewMsgDrawDebt(acc.GetAddress(), randCollateralParam.Denom, sdk.NewCoins(sdk.NewCoin(randDebtParam.Denom, randDrawAmount)))
err := msg.ValidateBasic() err := msg.ValidateBasic()
if err != nil { if err != nil {