mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-23 23:05:18 +00:00
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:
parent
ab72433db0
commit
22dc15f757
@ -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
|
||||
|
@ -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()))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user