0g-chain/x/cdp/keeper/draw.go
Kevin Davis ae4aee46ff
Use sdk.Coin in cdp module (#466)
* Use sdk.Coin in cdp module
Co-authored-by: Federico Kunze <federico.kunze94@gmail.com>
Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
Co-authored-by: Denali Marsh <denali@kava.io>
Co-authored-by: John Maheswaran <john@noreply>
2020-04-27 10:40:34 -04:00

221 lines
7.5 KiB
Go

package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/cdp/types"
)
// AddPrincipal adds debt to a cdp if the additional debt does not put the cdp below the liquidation ratio
func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, principal sdk.Coin) error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", owner, denom)
}
err := k.ValidatePrincipalDraw(ctx, principal, cdp.Principal.Denom)
if err != nil {
return err
}
err = k.ValidateDebtLimit(ctx, cdp.Collateral.Denom, principal)
if err != nil {
return err
}
err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal.Add(principal), cdp.AccumulatedFees)
if err != nil {
return err
}
// mint the principal and send it to the cdp owner
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(principal))
if err != nil {
panic(err)
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, sdk.NewCoins(principal))
if err != nil {
panic(err)
}
// mint the corresponding amount of debt coins in the cdp module account
err = k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), principal)
if err != nil {
panic(err)
}
// emit cdp draw event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpDraw,
sdk.NewAttribute(sdk.AttributeKeyAmount, principal.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
// remove old collateral:debt index
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
cdp.Principal = cdp.Principal.Add(principal)
// increment total principal for the input collateral type
k.IncrementTotalPrincipal(ctx, cdp.Collateral.Denom, principal)
// set cdp state and indexes in the store
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
return nil
}
// RepayPrincipal removes debt from the cdp
// If all debt is repaid, the collateral is returned to depositors and the cdp is removed from the store
func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, payment sdk.Coin) error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", owner, denom)
}
err := k.ValidatePaymentCoins(ctx, cdp, payment, cdp.Principal.Add(cdp.AccumulatedFees))
if err != nil {
return err
}
// calculate fee and principal payment
feePayment, principalPayment := k.calculatePayment(ctx, cdp.Principal.Add(cdp.AccumulatedFees), cdp.AccumulatedFees, payment)
// send the payment from the sender to the cpd module
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, sdk.NewCoins(feePayment.Add(principalPayment)))
if err != nil {
return err
}
// burn the payment coins
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(feePayment.Add(principalPayment)))
if err != nil {
panic(err)
}
// burn the corresponding amount of debt coins
cdpDebt := k.getModAccountDebt(ctx, types.ModuleName)
paymentAmount := feePayment.Amount.Add(principalPayment.Amount)
coinsToBurn := sdk.NewCoin(k.GetDebtDenom(ctx), paymentAmount)
if paymentAmount.GT(cdpDebt) {
coinsToBurn = sdk.NewCoin(k.GetDebtDenom(ctx), cdpDebt)
}
err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), coinsToBurn)
if err != nil {
panic(err)
}
// emit repayment event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpRepay,
sdk.NewAttribute(sdk.AttributeKeyAmount, feePayment.Add(principalPayment).String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
// remove the old collateral:debt ratio index
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
if !principalPayment.IsZero() {
cdp.Principal = cdp.Principal.Sub(principalPayment)
}
cdp.AccumulatedFees = cdp.AccumulatedFees.Sub(feePayment)
// decrement the total principal for the input collateral type
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
if cdp.Principal.IsZero() && cdp.AccumulatedFees.IsZero() {
k.ReturnCollateral(ctx, cdp)
k.DeleteCDP(ctx, cdp)
k.RemoveCdpOwnerIndex(ctx, cdp)
// emit cdp close event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpClose,
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
return nil
}
// set cdp state and update indexes
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
return nil
}
// ValidatePaymentCoins validates that the input coins are valid for repaying debt
func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coin, debt sdk.Coin) error {
if payment.Denom != debt.Denom {
return sdkerrors.Wrapf(types.ErrInvalidPayment, "cdp %d: expected %s, got %s", cdp.ID, debt.Denom, payment.Denom)
}
dp, found := k.GetDebtParam(ctx, payment.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidPayment, "payment denom %s not found", payment.Denom)
}
proposedBalance := cdp.Principal.Amount.Sub(payment.Amount)
if proposedBalance.GT(sdk.ZeroInt()) && proposedBalance.LT(dp.DebtFloor) {
return sdkerrors.Wrapf(types.ErrBelowDebtFloor, "proposed %s < minimum %s", sdk.NewCoin(payment.Denom, proposedBalance), dp.DebtFloor)
}
return nil
}
// ReturnCollateral returns collateral to depositors on a cdp and removes deposits from the store
func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) {
deposits := k.GetDeposits(ctx, cdp.ID)
for _, deposit := range deposits {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, deposit.Depositor, sdk.NewCoins(deposit.Amount))
if err != nil {
panic(err)
}
k.DeleteDeposit(ctx, cdp.ID, deposit.Depositor)
}
}
func (k Keeper) calculatePayment(ctx sdk.Context, owed sdk.Coin, fees sdk.Coin, payment sdk.Coin) (sdk.Coin, sdk.Coin) {
// divides repayment into principal and fee components, with fee payment applied first.
feePayment := sdk.NewCoin(payment.Denom, sdk.ZeroInt())
principalPayment := sdk.NewCoin(payment.Denom, sdk.ZeroInt())
overpayment := sdk.NewCoin(payment.Denom, sdk.ZeroInt())
if !payment.Amount.IsPositive() {
return feePayment, principalPayment
}
// check for over payment
if payment.Amount.GT(owed.Amount) {
overpayment = payment.Sub(owed)
payment = payment.Sub(overpayment)
}
// if no fees, 100% of payment is principal payment
if fees.IsZero() {
return feePayment, payment
}
// pay fees before repaying principal
if payment.Amount.GT(fees.Amount) {
feePayment = fees
principalPayment = payment.Sub(fees)
} else {
feePayment = payment
}
return feePayment, principalPayment
}