diff --git a/x/cdp/alias.go b/x/cdp/alias.go index dd553c82..78080754 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -28,6 +28,7 @@ const ( CodeCdpNotAvailable = types.CodeCdpNotAvailable CodeBelowDebtFloor = types.CodeBelowDebtFloor CodePaymentExceedsDebt = types.CodePaymentExceedsDebt + CodeLoadingAugmentedCDP = types.CodeLoadingAugmentedCDP EventTypeCreateCdp = types.EventTypeCreateCdp EventTypeCdpDeposit = types.EventTypeCdpDeposit EventTypeCdpDraw = types.EventTypeCdpDraw @@ -76,6 +77,7 @@ var ( ErrCdpNotAvailable = types.ErrCdpNotAvailable ErrBelowDebtFloor = types.ErrBelowDebtFloor ErrPaymentExceedsDebt = types.ErrPaymentExceedsDebt + ErrLoadingAugmentedCDP = types.ErrLoadingAugmentedCDP DefaultGenesisState = types.DefaultGenesisState GetCdpIDBytes = types.GetCdpIDBytes GetCdpIDFromBytes = types.GetCdpIDFromBytes @@ -143,6 +145,8 @@ var ( type ( CDP = types.CDP CDPs = types.CDPs + AugmentedCDP = types.AugmentedCDP + AugmentedCDPs = types.AugmentedCDPs Deposit = types.Deposit Deposits = types.Deposits SupplyKeeper = types.SupplyKeeper diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index 01247bd7..30a8ff08 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -69,7 +69,7 @@ $ %s query %s cdp kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw uatom } // Decode and print results - var cdp types.CDP + var cdp types.AugmentedCDP cdc.MustUnmarshalJSON(res, &cdp) return cliCtx.PrintOutput(cdp) }, @@ -105,9 +105,9 @@ $ %s query %s cdps uatom } // Decode and print results - var out types.CDPs - cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + var cdps types.AugmentedCDPs + cdc.MustUnmarshalJSON(res, &cdps) + return cliCtx.PrintOutput(cdps) }, } } @@ -119,7 +119,7 @@ func QueryCdpsByDenomAndRatioCmd(queryRoute string, cdc *codec.Codec) *cobra.Com Use: "cdps-by-ratio [collateral-name] [collateralization-ratio]", Short: "get cdps under a collateralization ratio", Long: strings.TrimSpace( - fmt.Sprintf(`List all CDPs under a collateralization ratios. + fmt.Sprintf(`List all CDPs under a specified collateralization ratio. Collateralization ratio is: collateral * price / debt. Example: @@ -150,9 +150,9 @@ $ %s query %s cdps-by-ratio uatom 1.5 } // Decode and print results - var out types.CDPs - cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + var cdps types.AugmentedCDPs + cdc.MustUnmarshalJSON(res, &cdps) + return cliCtx.PrintOutput(cdps) }, } } diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 09800b2c..3fb52fe4 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -9,6 +9,9 @@ import ( "github.com/kava-labs/kava/x/cdp/types" ) +// BaseDigitFactor is 10**18, used during coin calculations +const BaseDigitFactor = 1000000000000000000 + // AddCdp adds a cdp for a specific owner and collateral type func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) sdk.Error { // validation @@ -410,6 +413,38 @@ func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.C return collateralBaseUnits.Quo(debtTotal) } +// LoadAugmentedCDP creates a new augmented CDP from an existing CDP +func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.AugmentedCDP, sdk.Error) { + // calculate additional fees + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + totalFees := cdp.AccumulatedFees.Add(fees) + + // calculate collateralization ratio + collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal, totalFees) + if err != nil { + return types.AugmentedCDP{}, err + } + + // total debt is the sum of all oustanding principal and fees + var totalDebt int64 + for _, principalCoin := range cdp.Principal { + totalDebt += principalCoin.Amount.Int64() + } + for _, feeCoin := range cdp.AccumulatedFees.Add(fees) { + totalDebt += feeCoin.Amount.Int64() + } + + // convert collateral value to debt coin + debtBaseAdjusted := sdk.NewDec(totalDebt).QuoInt64(BaseDigitFactor) + collateralValueInDebtDenom := collateralizationRatio.Mul(debtBaseAdjusted) + collateralValueInDebt := sdk.NewInt64Coin(cdp.Principal[0].Denom, collateralValueInDebtDenom.Int64()) + + // create new augmuented cdp + augmentedCDP := types.NewAugmentedCDP(cdp, collateralValueInDebt, collateralizationRatio) + return augmentedCDP, nil +} + // CalculateCollateralizationRatio returns the collateralization ratio of the input collateral to the input debt plus fees func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, sdk.Error) { if collateral.IsZero() { @@ -422,6 +457,7 @@ func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk. } collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0]) collateralValue := collateralBaseUnits.Mul(price.Price) + principalTotal := sdk.ZeroDec() for _, pc := range principal { prinicpalBaseUnits := k.convertDebtToBaseUnits(ctx, pc) @@ -435,6 +471,19 @@ func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk. return collateralRatio, nil } +// CalculateCollateralizationRatioFromAbsoluteRatio takes a coin's denom and an absolute ratio and returns the respective collateralization ratio +func (k Keeper) CalculateCollateralizationRatioFromAbsoluteRatio(ctx sdk.Context, collateralDenom string, absoluteRatio sdk.Dec) (sdk.Dec, sdk.Error) { + // get price collateral + marketID := k.getMarketID(ctx, collateralDenom) + price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) + if err != nil { + return sdk.Dec{}, err + } + // convert absolute ratio to collateralization ratio + respectiveCollateralRatio := absoluteRatio.Quo(price.Price) + return respectiveCollateralRatio, nil +} + // converts the input collateral to base units (ie multiplies the input by 10^(-ConversionFactor)) func (k Keeper) convertCollateralToBaseUnits(ctx sdk.Context, collateral sdk.Coin) (baseUnits sdk.Dec) { cp, _ := k.GetCollateral(ctx, collateral.Denom) diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go index dada19ca..fdd8c86f 100644 --- a/x/cdp/keeper/querier.go +++ b/x/cdp/keeper/querier.go @@ -44,7 +44,12 @@ func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, return nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom) } - bz, err := codec.MarshalJSONIndent(keeper.cdc, cdp) + augmentedCDP, err := keeper.LoadAugmentedCDP(ctx, cdp) + if err != nil { + return nil, types.ErrLoadingAugmentedCDP(keeper.codespace, cdp.ID) + } + + bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDP) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } @@ -64,8 +69,21 @@ func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom) } - cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio) - bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps) + ratio, err := keeper.CalculateCollateralizationRatioFromAbsoluteRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could get collateralization ratio from absolute ratio", err.Error())) + } + + cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, ratio) + // augment CDPs by adding collateral value and collateralization ratio + var augmentedCDPs types.AugmentedCDPs + for _, cdp := range cdps { + augmentedCDP, err := keeper.LoadAugmentedCDP(ctx, cdp) + if err == nil { + augmentedCDPs = append(augmentedCDPs, augmentedCDP) + } + } + bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDPs) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } @@ -85,7 +103,15 @@ func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) } cdps := keeper.GetAllCdpsByDenom(ctx, requestParams.CollateralDenom) - bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps) + // augment CDPs by adding collateral value and collateralization ratio + var augmentedCDPs types.AugmentedCDPs + for _, cdp := range cdps { + augmentedCDP, err := keeper.LoadAugmentedCDP(ctx, cdp) + if err == nil { + augmentedCDPs = append(augmentedCDPs, augmentedCDP) + } + } + bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDPs) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } diff --git a/x/cdp/keeper/querier_test.go b/x/cdp/keeper/querier_test.go index d13ec917..0f2310e8 100644 --- a/x/cdp/keeper/querier_test.go +++ b/x/cdp/keeper/querier_test.go @@ -5,12 +5,15 @@ import ( "sort" "strings" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp/keeper" "github.com/kava-labs/kava/x/cdp/types" + pfkeeper "github.com/kava-labs/kava/x/pricefeed/keeper" + pftypes "github.com/kava-labs/kava/x/pricefeed/types" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" tmtime "github.com/tendermint/tendermint/types/time" @@ -23,18 +26,21 @@ const ( type QuerierTestSuite struct { suite.Suite - keeper keeper.Keeper - addrs []sdk.AccAddress - app app.TestApp - cdps types.CDPs - ctx sdk.Context - querier sdk.Querier + keeper keeper.Keeper + pricefeedKeeper pfkeeper.Keeper + addrs []sdk.AccAddress + app app.TestApp + cdps types.CDPs + augmentedCDPs types.AugmentedCDPs + ctx sdk.Context + querier sdk.Querier } func (suite *QuerierTestSuite) SetupTest() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) cdps := make(types.CDPs, 100) + augmentedCDPs := make(types.AugmentedCDPs, 100) _, addrs := app.GeneratePrivKeyAddressPairs(100) coins := []sdk.Coins{} @@ -53,6 +59,30 @@ func (suite *QuerierTestSuite) SetupTest() { suite.ctx = ctx suite.app = tApp suite.keeper = tApp.GetCDPKeeper() + suite.pricefeedKeeper = tApp.GetPriceFeedKeeper() + + // Set up markets + oracle := addrs[9] + marketParams := pftypes.Params{ + Markets: pftypes.Markets{ + pftypes.Market{MarketID: "xrp-usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{oracle}, Active: true}, + pftypes.Market{MarketID: "btc-usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{oracle}, Active: true}, + }, + } + suite.pricefeedKeeper.SetParams(ctx, marketParams) + + // Set collateral prices for use in collateralization calculations + _, err := suite.pricefeedKeeper.SetPrice( + ctx, oracle, "xrp-usd", + sdk.MustNewDecFromStr("0.75"), + time.Now().Add(1*time.Hour)) + suite.Nil(err) + + _, err = suite.pricefeedKeeper.SetPrice( + ctx, oracle, "btc-usd", + sdk.MustNewDecFromStr("5000"), + time.Now().Add(1*time.Hour)) + suite.Nil(err) for j := 0; j < 100; j++ { collateral := "xrp" @@ -67,9 +97,12 @@ func (suite *QuerierTestSuite) SetupTest() { c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1)) suite.True(f) cdps[j] = c + aCDP, _ := suite.keeper.LoadAugmentedCDP(suite.ctx, c) + augmentedCDPs[j] = aCDP } suite.cdps = cdps + suite.augmentedCDPs = augmentedCDPs suite.querier = keeper.NewQuerier(suite.keeper) suite.addrs = addrs } @@ -84,9 +117,9 @@ func (suite *QuerierTestSuite) TestQueryCdp() { suite.Nil(err) suite.NotNil(bz) - var c types.CDP + var c types.AugmentedCDP suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) - suite.Equal(suite.cdps[0], c) + suite.Equal(suite.augmentedCDPs[0], c) query = abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"), @@ -125,7 +158,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByDenom() { suite.Nil(err) suite.NotNil(bz) - var c types.CDPs + var c types.AugmentedCDPs suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) suite.Equal(50, len(c)) @@ -140,19 +173,21 @@ func (suite *QuerierTestSuite) TestQueryCdpsByDenom() { func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { ratioCountBtc := 0 ratioCountXrp := 0 - xrpRatio := d("50.0") - btcRatio := d("0.003") + xrpRatio := d("2.0") + btcRatio := d("2500") expectedXrpIds := []int{} expectedBtcIds := []int{} for _, cdp := range suite.cdps { - r := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + absoluteRatio := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + collateralizationRatio, err := suite.keeper.CalculateCollateralizationRatioFromAbsoluteRatio(suite.ctx, cdp.Collateral[0].Denom, absoluteRatio) + suite.Nil(err) if cdp.Collateral[0].Denom == "xrp" { - if r.LT(xrpRatio) { + if collateralizationRatio.LT(xrpRatio) { ratioCountXrp += 1 expectedXrpIds = append(expectedXrpIds, int(cdp.ID)) } } else { - if r.LT(btcRatio) { + if collateralizationRatio.LT(btcRatio) { ratioCountBtc += 1 expectedBtcIds = append(expectedBtcIds, int(cdp.ID)) } @@ -168,7 +203,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { suite.Nil(err) suite.NotNil(bz) - var c types.CDPs + var c types.AugmentedCDPs actualXrpIds := []int{} suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) for _, k := range c { @@ -185,7 +220,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { suite.Nil(err) suite.NotNil(bz) - c = types.CDPs{} + c = types.AugmentedCDPs{} actualBtcIds := []int{} suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) for _, k := range c { @@ -201,7 +236,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { bz, err = suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query) suite.Nil(err) suite.NotNil(bz) - c = types.CDPs{} + c = types.AugmentedCDPs{} suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) suite.Equal(0, len(c)) } diff --git a/x/cdp/types/cdp.go b/x/cdp/types/cdp.go index c807b12a..8c5365be 100644 --- a/x/cdp/types/cdp.go +++ b/x/cdp/types/cdp.go @@ -62,3 +62,63 @@ func (cdps CDPs) String() string { } return out } + +// AugmentedCDP provides additional information about an active CDP +type AugmentedCDP struct { + CDP `json:"cdp" yaml:"cdp"` + CollateralValue sdk.Coin `json:"collateral_value" yaml:"collateral_value"` // collateral's market value in debt coin + CollateralizationRatio sdk.Dec `json:"collateralization_ratio" yaml:"collateralization_ratio"` // current collateralization ratio +} + +// NewAugmentedCDP creates a new AugmentedCDP object +func NewAugmentedCDP(cdp CDP, collateralValue sdk.Coin, collateralizationRatio sdk.Dec) AugmentedCDP { + augmentedCDP := AugmentedCDP{ + CDP: CDP{ + ID: cdp.ID, + Owner: cdp.Owner, + Collateral: cdp.Collateral, + Principal: cdp.Principal, + AccumulatedFees: cdp.AccumulatedFees, + FeesUpdated: cdp.FeesUpdated, + }, + CollateralValue: collateralValue, + CollateralizationRatio: collateralizationRatio, + } + return augmentedCDP +} + +// String implements fmt.stringer +func (augCDP AugmentedCDP) String() string { + return strings.TrimSpace(fmt.Sprintf(`AugmentedCDP: + Owner: %s + ID: %d + Collateral Type: %s + Collateral: %s + Collateral Value: %s + Principal: %s + Fees: %s + Fees Last Updated: %s + Collateralization ratio: %s`, + augCDP.Owner, + augCDP.ID, + augCDP.Collateral[0].Denom, + augCDP.Collateral, + augCDP.CollateralValue, + augCDP.Principal, + augCDP.AccumulatedFees, + augCDP.FeesUpdated, + augCDP.CollateralizationRatio, + )) +} + +// AugmentedCDPs a collection of AugmentedCDP objects +type AugmentedCDPs []AugmentedCDP + +// String implements stringer +func (augcdps AugmentedCDPs) String() string { + out := "" + for _, augcdp := range augcdps { + out += augcdp.String() + "\n" + } + return out +} diff --git a/x/cdp/types/errors.go b/x/cdp/types/errors.go index 56cd5a93..95dc05b9 100644 --- a/x/cdp/types/errors.go +++ b/x/cdp/types/errors.go @@ -26,6 +26,7 @@ const ( CodeCdpNotAvailable sdk.CodeType = 14 CodeBelowDebtFloor sdk.CodeType = 15 CodePaymentExceedsDebt sdk.CodeType = 16 + CodeLoadingAugmentedCDP sdk.CodeType = 17 ) // ErrCdpAlreadyExists error for duplicate cdps @@ -107,3 +108,8 @@ func ErrBelowDebtFloor(codespace sdk.CodespaceType, debt sdk.Coins, floor sdk.In func ErrPaymentExceedsDebt(codespace sdk.CodespaceType, payment sdk.Coins, principal sdk.Coins) sdk.Error { return sdk.NewError(codespace, CodePaymentExceedsDebt, fmt.Sprintf("payment of %s exceeds debt of %s", payment, principal)) } + +// ErrLoadingAugmentedCDP error loading augmented cdp +func ErrLoadingAugmentedCDP(codespace sdk.CodespaceType, cdpID uint64) sdk.Error { + return sdk.NewError(codespace, CodeCdpNotFound, fmt.Sprintf("augmented cdp could not be loaded from cdp id %d", cdpID)) +}