mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 15:25: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
|
CodeInvalidWithdrawAmount = types.CodeInvalidWithdrawAmount
|
||||||
CodeCdpNotAvailable = types.CodeCdpNotAvailable
|
CodeCdpNotAvailable = types.CodeCdpNotAvailable
|
||||||
CodeBelowDebtFloor = types.CodeBelowDebtFloor
|
CodeBelowDebtFloor = types.CodeBelowDebtFloor
|
||||||
|
CodePaymentExceedsDebt = types.CodePaymentExceedsDebt
|
||||||
EventTypeCreateCdp = types.EventTypeCreateCdp
|
EventTypeCreateCdp = types.EventTypeCreateCdp
|
||||||
EventTypeCdpDeposit = types.EventTypeCdpDeposit
|
EventTypeCdpDeposit = types.EventTypeCdpDeposit
|
||||||
EventTypeCdpDraw = types.EventTypeCdpDraw
|
EventTypeCdpDraw = types.EventTypeCdpDraw
|
||||||
@ -74,6 +75,7 @@ var (
|
|||||||
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
|
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
|
||||||
ErrCdpNotAvailable = types.ErrCdpNotAvailable
|
ErrCdpNotAvailable = types.ErrCdpNotAvailable
|
||||||
ErrBelowDebtFloor = types.ErrBelowDebtFloor
|
ErrBelowDebtFloor = types.ErrBelowDebtFloor
|
||||||
|
ErrPaymentExceedsDebt = types.ErrPaymentExceedsDebt
|
||||||
DefaultGenesisState = types.DefaultGenesisState
|
DefaultGenesisState = types.DefaultGenesisState
|
||||||
GetCdpIDBytes = types.GetCdpIDBytes
|
GetCdpIDBytes = types.GetCdpIDBytes
|
||||||
GetCdpIDFromBytes = types.GetCdpIDFromBytes
|
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
|
// 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 {
|
func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.Error {
|
||||||
for _, dc := range principal {
|
for _, dc := range principal {
|
||||||
dp, found := k.GetDebt(ctx, dc.Denom)
|
dp, found := k.GetDebtParam(ctx, dc.Denom)
|
||||||
if !found {
|
if !found {
|
||||||
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
|
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
|
// 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 {
|
func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.Error {
|
||||||
for _, dc := range principal {
|
for _, dc := range principal {
|
||||||
_, found := k.GetDebt(ctx, dc.Denom)
|
_, found := k.GetDebtParam(ctx, dc.Denom)
|
||||||
if !found {
|
if !found {
|
||||||
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
|
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))
|
// 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) {
|
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()))
|
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 {
|
if !found {
|
||||||
return types.ErrCdpNotFound(k.codespace, owner, denom)
|
return types.ErrCdpNotFound(k.codespace, owner, denom)
|
||||||
}
|
}
|
||||||
err := k.ValidatePaymentCoins(ctx, cdp, payment)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate fees
|
// calculate fees
|
||||||
periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
|
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)
|
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
|
// 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
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// burn the payment coins
|
// 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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// burn the corresponding amount of debt coins
|
// 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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri
|
|||||||
ctx.EventManager().EmitEvent(
|
ctx.EventManager().EmitEvent(
|
||||||
sdk.NewEvent(
|
sdk.NewEvent(
|
||||||
types.EventTypeCdpRepay,
|
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)),
|
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()
|
cdp.FeesUpdated = ctx.BlockTime()
|
||||||
|
|
||||||
// decrement the total principal for the input collateral type
|
// 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,
|
// if the debt is fully paid, return collateral to depositors,
|
||||||
// and remove the cdp and indexes from the store
|
// 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
|
// 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)
|
subset := payment.DenomsSubsetOf(cdp.Principal)
|
||||||
if !subset {
|
if !subset {
|
||||||
var paymentDenoms []string
|
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)
|
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
|
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.
|
// divides repayment into principal and fee components, with fee payment applied first.
|
||||||
|
|
||||||
feePayment := sdk.NewCoins()
|
feePayment := sdk.NewCoins()
|
||||||
principalPayment := 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() {
|
if fees.IsZero() {
|
||||||
return sdk.NewCoins(), payment
|
return sdk.NewCoins(), payment
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func (suite *DrawTestSuite) SetupTest() {
|
|||||||
authGS := app.NewAuthGenState(
|
authGS := app.NewAuthGenState(
|
||||||
addrs,
|
addrs,
|
||||||
[]sdk.Coins{
|
[]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", 200000000)),
|
||||||
cs(c("xrp", 10000000000000), c("usdx", 100000000000))})
|
cs(c("xrp", 10000000000000), c("usdx", 100000000000))})
|
||||||
tApp.InitializeFromGenesisStates(
|
tApp.InitializeFromGenesisStates(
|
||||||
@ -126,9 +126,9 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() {
|
|||||||
suite.Equal(types.CodeInvalidPaymentDenom, err.Result().Code)
|
suite.Equal(types.CodeInvalidPaymentDenom, err.Result().Code)
|
||||||
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000)))
|
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000)))
|
||||||
suite.Equal(types.CodeCdpNotFound, err.Result().Code)
|
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)))
|
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
|
||||||
suite.NoError(err)
|
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() {
|
func (suite *DrawTestSuite) TestAddRepayPrincipalFees() {
|
||||||
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000)))
|
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000)))
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
@ -176,9 +186,9 @@ func (suite *DrawTestSuite) TestPricefeedFailure() {
|
|||||||
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
|
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
|
||||||
pfk := suite.app.GetPriceFeedKeeper()
|
pfk := suite.app.GetPriceFeedKeeper()
|
||||||
pfk.SetCurrentPrices(ctx, "xrp:usd")
|
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)
|
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)
|
suite.NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +199,7 @@ func (suite *DrawTestSuite) TestModuleAccountFailure() {
|
|||||||
acc := sk.GetModuleAccount(ctx, types.ModuleName)
|
acc := sk.GetModuleAccount(ctx, types.ModuleName)
|
||||||
ak := suite.app.GetAccountKeeper()
|
ak := suite.app.GetAccountKeeper()
|
||||||
ak.RemoveAccount(ctx, acc)
|
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
|
return types.CollateralParam{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDebt returns the debt param with matching denom
|
// GetDebtParam returns the debt param with matching denom
|
||||||
func (k Keeper) GetDebt(ctx sdk.Context, denom string) (types.DebtParam, bool) {
|
func (k Keeper) GetDebtParam(ctx sdk.Context, denom string) (types.DebtParam, bool) {
|
||||||
params := k.GetParams(ctx)
|
params := k.GetParams(ctx)
|
||||||
for _, dp := range params.DebtParams {
|
for _, dp := range params.DebtParams {
|
||||||
if dp.Denom == denom {
|
if dp.Denom == denom {
|
||||||
|
@ -25,6 +25,7 @@ const (
|
|||||||
CodeInvalidWithdrawAmount sdk.CodeType = 13
|
CodeInvalidWithdrawAmount sdk.CodeType = 13
|
||||||
CodeCdpNotAvailable sdk.CodeType = 14
|
CodeCdpNotAvailable sdk.CodeType = 14
|
||||||
CodeBelowDebtFloor sdk.CodeType = 15
|
CodeBelowDebtFloor sdk.CodeType = 15
|
||||||
|
CodePaymentExceedsDebt sdk.CodeType = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrCdpAlreadyExists error for duplicate cdps
|
// 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 {
|
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))
|
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