mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 06:59:41 +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