R4R: add collateral value, collateralization ratio to CDP querier (#347)

* AugmentedCDP type, codec registration, querier update

* added unique error for augmented cdp loading

* added AugmentedCDPs type for cdps query res

* query results for cdps (by denom) & cdps-by-ratio (by denom & ratio)

* status: converting collateral value into debt coin denom

* collateral value denominated in debt coin

* query cdps-by-ratio now searches by collateralization ratio instead of absolute ratio

* updated alias, code comments

* updated querier tests

* support multiple principal coins and their associated fees

* collateralization ratio calculations on updated fees

* include calculated fees in total debt calculation
This commit is contained in:
Denali Marsh 2020-01-28 09:47:08 -08:00 committed by GitHub
parent 075a3089ce
commit bf64a5c02c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 209 additions and 29 deletions

View File

@ -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

View File

@ -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)
},
}
}

View File

@ -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)

View File

@ -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()))
}

View File

@ -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"
@ -24,9 +27,11 @@ type QuerierTestSuite struct {
suite.Suite
keeper keeper.Keeper
pricefeedKeeper pfkeeper.Keeper
addrs []sdk.AccAddress
app app.TestApp
cdps types.CDPs
augmentedCDPs types.AugmentedCDPs
ctx sdk.Context
querier sdk.Querier
}
@ -35,6 +40,7 @@ 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))
}

View File

@ -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
}

View File

@ -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))
}