Apply debt floor to repayments (#314)

* fix: remove redundant debt limit param

* wip: test pricefeed genesis

* fix: pricefeed querier

* fix: comments, naming

* fix: query path

* fix: store methods

* fix: query methods

* feat: Liquidation Penalty

* feat: enforce debt floor on repayment

* address review comments

* fix: remove debt from liquidation penalty

* test: remove payment > balance check

* feat: handle overpayment

* fix: avoid negative coins error for overpayments
This commit is contained in:
Kevin Davis 2020-01-22 16:50:27 +00:00 committed by GitHub
parent ab72433db0
commit 22dc15f757
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 55 additions and 23 deletions

View File

@ -27,6 +27,7 @@ const (
CodeInvalidWithdrawAmount = types.CodeInvalidWithdrawAmount
CodeCdpNotAvailable = types.CodeCdpNotAvailable
CodeBelowDebtFloor = types.CodeBelowDebtFloor
CodePaymentExceedsDebt = types.CodePaymentExceedsDebt
EventTypeCreateCdp = types.EventTypeCreateCdp
EventTypeCdpDeposit = types.EventTypeCdpDeposit
EventTypeCdpDraw = types.EventTypeCdpDraw
@ -74,6 +75,7 @@ var (
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
ErrCdpNotAvailable = types.ErrCdpNotAvailable
ErrBelowDebtFloor = types.ErrBelowDebtFloor
ErrPaymentExceedsDebt = types.ErrPaymentExceedsDebt
DefaultGenesisState = types.DefaultGenesisState
GetCdpIDBytes = types.GetCdpIDBytes
GetCdpIDFromBytes = types.GetCdpIDFromBytes

View File

@ -346,7 +346,7 @@ func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) sdk.Er
// ValidatePrincipalAdd validates that an asset is valid for use as debt when creating a new cdp
func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.Error {
for _, dc := range principal {
dp, found := k.GetDebt(ctx, dc.Denom)
dp, found := k.GetDebtParam(ctx, dc.Denom)
if !found {
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
}
@ -360,7 +360,7 @@ func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.E
// ValidatePrincipalDraw validates that an asset is valid for use as debt when drawing debt off an existing cdp
func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.Error {
for _, dc := range principal {
_, found := k.GetDebt(ctx, dc.Denom)
_, found := k.GetDebtParam(ctx, dc.Denom)
if !found {
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
}
@ -440,6 +440,6 @@ func (k Keeper) convertCollateralToBaseUnits(ctx sdk.Context, collateral sdk.Coi
// converts the input debt to base units (ie multiplies the input by 10^(-ConversionFactor))
func (k Keeper) convertDebtToBaseUnits(ctx sdk.Context, debt sdk.Coin) (baseUnits sdk.Dec) {
dp, _ := k.GetDebt(ctx, debt.Denom)
dp, _ := k.GetDebtParam(ctx, debt.Denom)
return sdk.NewDecFromInt(debt.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), dp.ConversionFactor.Int64()))
}

View File

@ -85,32 +85,32 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
if !found {
return types.ErrCdpNotFound(k.codespace, owner, denom)
}
err := k.ValidatePaymentCoins(ctx, cdp, payment)
if err != nil {
return err
}
// calculate 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)
err := k.ValidatePaymentCoins(ctx, cdp, payment, cdp.Principal.Add(cdp.AccumulatedFees).Add(fees))
if err != nil {
return err
}
// calculate fee and principal payment
feePayment, principalPayment := k.calculatePayment(ctx, cdp.AccumulatedFees.Add(fees), payment)
feePayment, principalPayment := k.calculatePayment(ctx, cdp.Principal.Add(cdp.AccumulatedFees).Add(fees), cdp.AccumulatedFees.Add(fees), payment)
// send the payment from the sender to the cpd module
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, payment)
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, feePayment.Add(principalPayment))
if err != nil {
return err
}
// burn the payment coins
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, payment)
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, feePayment.Add(principalPayment))
if err != nil {
panic(err)
}
// burn the corresponding amount of debt coins
err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), payment)
err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), feePayment.Add(principalPayment))
if err != nil {
panic(err)
}
@ -119,7 +119,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpRepay,
sdk.NewAttribute(sdk.AttributeKeyAmount, payment.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, feePayment.Add(principalPayment).String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
@ -136,7 +136,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
cdp.FeesUpdated = ctx.BlockTime()
// decrement the total principal for the input collateral type
k.DecrementTotalPrincipal(ctx, denom, payment)
k.DecrementTotalPrincipal(ctx, denom, feePayment.Add(principalPayment))
// if the debt is fully paid, return collateral to depositors,
// and remove the cdp and indexes from the store
@ -162,7 +162,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
}
// ValidatePaymentCoins validates that the input coins are valid for repaying debt
func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins) sdk.Error {
func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins, debt sdk.Coins) sdk.Error {
subset := payment.DenomsSubsetOf(cdp.Principal)
if !subset {
var paymentDenoms []string
@ -175,6 +175,13 @@ func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk
}
return types.ErrInvalidPaymentDenom(k.codespace, cdp.ID, principalDenoms, paymentDenoms)
}
for _, dc := range payment {
dp, _ := k.GetDebtParam(ctx, dc.Denom)
proposedBalance := cdp.Principal.AmountOf(dc.Denom).Sub(dc.Amount)
if proposedBalance.GT(sdk.ZeroInt()) && proposedBalance.LT(dp.DebtFloor) {
return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, proposedBalance)), dp.DebtFloor)
}
}
return nil
}
@ -190,10 +197,17 @@ func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) {
}
}
func (k Keeper) calculatePayment(ctx sdk.Context, fees sdk.Coins, payment sdk.Coins) (sdk.Coins, sdk.Coins) {
func (k Keeper) calculatePayment(ctx sdk.Context, owed sdk.Coins, fees sdk.Coins, payment sdk.Coins) (sdk.Coins, sdk.Coins) {
// divides repayment into principal and fee components, with fee payment applied first.
feePayment := sdk.NewCoins()
principalPayment := sdk.NewCoins()
overpayment := sdk.NewCoins()
// TODO must compare denoms directly if there are multiple principal denoms
if payment.IsAllGT(owed) {
overpayment = payment.Sub(owed)
payment = payment.Sub(overpayment)
}
if fees.IsZero() {
return sdk.NewCoins(), payment
}

View File

@ -29,7 +29,7 @@ func (suite *DrawTestSuite) SetupTest() {
authGS := app.NewAuthGenState(
addrs,
[]sdk.Coins{
cs(c("xrp", 500000000), c("btc", 500000000)),
cs(c("xrp", 500000000), c("btc", 500000000), c("usdx", 10000000000)),
cs(c("xrp", 200000000)),
cs(c("xrp", 10000000000000), c("usdx", 100000000000))})
tApp.InitializeFromGenesisStates(
@ -126,9 +126,9 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
suite.Equal(types.CodeInvalidPaymentDenom, err.Result().Code)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000)))
suite.Equal(types.CodeCdpNotFound, err.Result().Code)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 100000000)))
suite.Error(err)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 9000000)))
suite.Equal(types.CodeBelowDebtFloor, err.Result().Code)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
@ -144,6 +144,16 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
}
func (suite *DrawTestSuite) TestRepayPrincipalOverpay() {
err := suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 20000000)))
suite.NoError(err)
ak := suite.app.GetAccountKeeper()
acc := ak.GetAccount(suite.ctx, suite.addrs[0])
suite.Equal(i(10000000000), (acc.GetCoins().AmountOf("usdx")))
_, found := suite.keeper.GetCDP(suite.ctx, "xrp", 1)
suite.False(found)
}
func (suite *DrawTestSuite) TestAddRepayPrincipalFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000)))
suite.NoError(err)
@ -176,9 +186,9 @@ func (suite *DrawTestSuite) TestPricefeedFailure() {
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
pfk := suite.app.GetPriceFeedKeeper()
pfk.SetCurrentPrices(ctx, "xrp:usd")
err := suite.keeper.AddPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10)))
err := suite.keeper.AddPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.Error(err)
err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10)))
err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
}
@ -189,7 +199,7 @@ func (suite *DrawTestSuite) TestModuleAccountFailure() {
acc := sk.GetModuleAccount(ctx, types.ModuleName)
ak := suite.app.GetAccountKeeper()
ak.RemoveAccount(ctx, acc)
_ = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10)))
_ = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
})
}

View File

@ -30,8 +30,8 @@ func (k Keeper) GetCollateral(ctx sdk.Context, denom string) (types.CollateralPa
return types.CollateralParam{}, false
}
// GetDebt returns the debt param with matching denom
func (k Keeper) GetDebt(ctx sdk.Context, denom string) (types.DebtParam, bool) {
// GetDebtParam returns the debt param with matching denom
func (k Keeper) GetDebtParam(ctx sdk.Context, denom string) (types.DebtParam, bool) {
params := k.GetParams(ctx)
for _, dp := range params.DebtParams {
if dp.Denom == denom {

View File

@ -25,6 +25,7 @@ const (
CodeInvalidWithdrawAmount sdk.CodeType = 13
CodeCdpNotAvailable sdk.CodeType = 14
CodeBelowDebtFloor sdk.CodeType = 15
CodePaymentExceedsDebt sdk.CodeType = 16
)
// ErrCdpAlreadyExists error for duplicate cdps
@ -101,3 +102,8 @@ func ErrCdpNotAvailable(codespace sdk.CodespaceType, cdpID uint64) sdk.Error {
func ErrBelowDebtFloor(codespace sdk.CodespaceType, debt sdk.Coins, floor sdk.Int) sdk.Error {
return sdk.NewError(codespace, CodeBelowDebtFloor, fmt.Sprintf("proposed cdp debt of %s is below the minimum of %s", debt, floor))
}
// ErrPaymentExceedsDebt error for repayments that are greater than the debt amount
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))
}