diff --git a/x/cdp/alias.go b/x/cdp/alias.go index 6d9d75e2..a54b1ada 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -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 diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 7a342990..0b41943f 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -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())) } diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go index f286e45a..54abede7 100644 --- a/x/cdp/keeper/draw.go +++ b/x/cdp/keeper/draw.go @@ -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 } diff --git a/x/cdp/keeper/draw_test.go b/x/cdp/keeper/draw_test.go index c8f97998..9239ffe8 100644 --- a/x/cdp/keeper/draw_test.go +++ b/x/cdp/keeper/draw_test.go @@ -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))) }) } diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go index 337af5a3..d0bd8a06 100644 --- a/x/cdp/keeper/params.go +++ b/x/cdp/keeper/params.go @@ -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 { diff --git a/x/cdp/types/errors.go b/x/cdp/types/errors.go index d7e14d73..56cd5a93 100644 --- a/x/cdp/types/errors.go +++ b/x/cdp/types/errors.go @@ -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)) +}