mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-10 10:05:18 +00:00
d849d690e5
* wip: tpyes and keeper methods * wip: iterators * wip: types and keeper methods * wip: add msgs * wip: client methods * wip: rebase develop * wip: types tests * wip: keeper tests, small fixes * wip: add cdp tests * wip: deposit tests * wip: keeper tests * wip: tests and module methods * feat: error when fetching expired price * feat: conversion factor for external assets * feat: debt floor for new cdps * feat: save deposits on export genesis * feat: ensure messages implement msg * feat: index deposits by status * fix: stray comment * wip: address review comments * address review comments
422 lines
14 KiB
Go
422 lines
14 KiB
Go
package keeper
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
|
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/kava-labs/kava/x/cdp/types"
|
|
)
|
|
|
|
// AddCdp adds a cdp for a specific owner and collateral type
|
|
func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) sdk.Error {
|
|
// validation
|
|
err := k.ValidateCollateral(ctx, collateral)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
|
|
if found {
|
|
return types.ErrCdpAlreadyExists(k.codespace, owner, collateral[0].Denom)
|
|
}
|
|
err = k.ValidatePrincipalAdd(ctx, principal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// send coins from the owners account to the cdp module
|
|
id := k.GetNextCdpID(ctx)
|
|
cdp := types.NewCDP(id, owner, collateral, principal, ctx.BlockHeader().Time)
|
|
deposit := types.NewDeposit(cdp.ID, owner, collateral)
|
|
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, collateral)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// mint the principal and send to the owners account
|
|
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, principal)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, principal)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// mint the corresponding amount of debt coins
|
|
err = k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), principal)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// emit events for cdp creation, deposit, and draw
|
|
ctx.EventManager().EmitEvent(
|
|
sdk.NewEvent(
|
|
types.EventTypeCreateCdp,
|
|
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
|
|
),
|
|
)
|
|
ctx.EventManager().EmitEvent(
|
|
sdk.NewEvent(
|
|
types.EventTypeCdpDeposit,
|
|
sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()),
|
|
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
|
|
),
|
|
)
|
|
ctx.EventManager().EmitEvent(
|
|
sdk.NewEvent(
|
|
types.EventTypeCdpDraw,
|
|
sdk.NewAttribute(sdk.AttributeKeyAmount, principal.String()),
|
|
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
|
|
),
|
|
)
|
|
|
|
// update total principal for input collateral type
|
|
k.IncrementTotalPrincipal(ctx, collateral[0].Denom, principal)
|
|
|
|
// set the cdp, deposit, and indexes in the store
|
|
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, principal)
|
|
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
|
|
k.IndexCdpByOwner(ctx, cdp)
|
|
k.SetDeposit(ctx, deposit)
|
|
k.SetNextCdpID(ctx, id+1)
|
|
return nil
|
|
}
|
|
|
|
// SetCdpAndCollateralRatioIndex sets the cdp and collateral ratio index in the store
|
|
func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ratio sdk.Dec) {
|
|
k.SetCDP(ctx, cdp)
|
|
k.IndexCdpByCollateralRatio(ctx, cdp.Collateral[0].Denom, cdp.ID, ratio)
|
|
}
|
|
|
|
// MintDebtCoins mints debt coins in the cdp module account
|
|
func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coins) sdk.Error {
|
|
coinsToMint := sdk.NewCoins()
|
|
for _, sc := range principalCoins {
|
|
coinsToMint = coinsToMint.Add(sdk.NewCoins(sdk.NewCoin(denom, sc.Amount)))
|
|
}
|
|
err := k.supplyKeeper.MintCoins(ctx, moduleAccount, coinsToMint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BurnDebtCoins burns debts coins from the cdp module account
|
|
func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coins) sdk.Error {
|
|
coinsToBurn := sdk.NewCoins()
|
|
for _, pc := range paymentCoins {
|
|
coinsToBurn = coinsToBurn.Add(sdk.NewCoins(sdk.NewCoin(denom, pc.Amount)))
|
|
}
|
|
err := k.supplyKeeper.BurnCoins(ctx, moduleAccount, coinsToBurn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetCdpID returns the id of the cdp corresponding to a specific owner and collateral denom
|
|
func (k Keeper) GetCdpID(ctx sdk.Context, owner sdk.AccAddress, denom string) (uint64, bool) {
|
|
|
|
cdpIDs, found := k.GetCdpIdsByOwner(ctx, owner)
|
|
if !found {
|
|
return 0, false
|
|
}
|
|
for _, id := range cdpIDs {
|
|
_, found = k.GetCDP(ctx, denom, id)
|
|
if found {
|
|
return id, true
|
|
}
|
|
}
|
|
return 0, false
|
|
|
|
}
|
|
|
|
// GetCdpIdsByOwner returns all the ids of cdps corresponding to a particular owner
|
|
func (k Keeper) GetCdpIdsByOwner(ctx sdk.Context, owner sdk.AccAddress) ([]uint64, bool) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix)
|
|
bz := store.Get(owner)
|
|
// TODO figure out why this is necessary
|
|
if bz == nil || bytes.Equal(bz, []byte{0}) {
|
|
return []uint64{}, false
|
|
}
|
|
var cdpIDs []uint64
|
|
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdpIDs)
|
|
return cdpIDs, true
|
|
}
|
|
|
|
// GetCdpByOwnerAndDenom queries cdps owned by owner and returns the cdp with matching denom
|
|
func (k Keeper) GetCdpByOwnerAndDenom(ctx sdk.Context, owner sdk.AccAddress, denom string) (types.CDP, bool) {
|
|
cdpIDs, found := k.GetCdpIdsByOwner(ctx, owner)
|
|
if !found {
|
|
return types.CDP{}, false
|
|
}
|
|
for _, id := range cdpIDs {
|
|
cdp, found := k.GetCDP(ctx, denom, id)
|
|
if found {
|
|
return cdp, true
|
|
}
|
|
}
|
|
return types.CDP{}, false
|
|
}
|
|
|
|
// GetCDP returns the cdp associated with a particular collateral denom and id
|
|
func (k Keeper) GetCDP(ctx sdk.Context, collateralDenom string, cdpID uint64) (types.CDP, bool) {
|
|
// get store
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
|
|
db, _ := k.GetDenomPrefix(ctx, collateralDenom)
|
|
// get CDP
|
|
bz := store.Get(types.CdpKey(db, cdpID))
|
|
// unmarshal
|
|
if bz == nil {
|
|
return types.CDP{}, false
|
|
}
|
|
var cdp types.CDP
|
|
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdp)
|
|
return cdp, true
|
|
}
|
|
|
|
// SetCDP sets a cdp in the store
|
|
func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
|
|
db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom)
|
|
bz := k.cdc.MustMarshalBinaryLengthPrefixed(cdp)
|
|
store.Set(types.CdpKey(db, cdp.ID), bz)
|
|
return
|
|
}
|
|
|
|
// DeleteCDP deletes a cdp from the store
|
|
func (k Keeper) DeleteCDP(ctx sdk.Context, cdp types.CDP) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
|
|
db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom)
|
|
store.Delete(types.CdpKey(db, cdp.ID))
|
|
|
|
}
|
|
|
|
// GetAllCdps returns all cdps from the store
|
|
func (k Keeper) GetAllCdps(ctx sdk.Context) (cdps types.CDPs) {
|
|
k.IterateAllCdps(ctx, func(cdp types.CDP) bool {
|
|
cdps = append(cdps, cdp)
|
|
return false
|
|
})
|
|
return
|
|
}
|
|
|
|
// GetAllCdpsByDenom returns all cdps of a particular collateral type from the store
|
|
func (k Keeper) GetAllCdpsByDenom(ctx sdk.Context, denom string) (cdps types.CDPs) {
|
|
k.IterateCdpsByDenom(ctx, denom, func(cdp types.CDP) bool {
|
|
cdps = append(cdps, cdp)
|
|
return false
|
|
})
|
|
return
|
|
}
|
|
|
|
// GetAllCdpsByDenomAndRatio returns all cdps of a particular collateral type and below a certain collateralization ratio
|
|
func (k Keeper) GetAllCdpsByDenomAndRatio(ctx sdk.Context, denom string, targetRatio sdk.Dec) (cdps types.CDPs) {
|
|
k.IterateCdpsByCollateralRatio(ctx, denom, targetRatio, func(cdp types.CDP) bool {
|
|
cdps = append(cdps, cdp)
|
|
return false
|
|
})
|
|
return
|
|
}
|
|
|
|
// SetNextCdpID sets the highest cdp id in the store
|
|
func (k Keeper) SetNextCdpID(ctx sdk.Context, id uint64) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKey)
|
|
store.Set([]byte{}, types.GetCdpIDBytes(id))
|
|
}
|
|
|
|
// GetNextCdpID returns the highest cdp id from the store
|
|
func (k Keeper) GetNextCdpID(ctx sdk.Context) (id uint64) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKey)
|
|
bz := store.Get([]byte{})
|
|
if bz == nil {
|
|
panic("starting cdp id not set in genesis")
|
|
}
|
|
id = types.GetCdpIDFromBytes(bz)
|
|
return
|
|
}
|
|
|
|
// IndexCdpByOwner sets the cdp id in the store, indexed by the owner
|
|
func (k Keeper) IndexCdpByOwner(ctx sdk.Context, cdp types.CDP) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix)
|
|
cdpIDs, found := k.GetCdpIdsByOwner(ctx, cdp.Owner)
|
|
|
|
if !found {
|
|
idBytes := k.cdc.MustMarshalBinaryLengthPrefixed([]uint64{cdp.ID})
|
|
store.Set(cdp.Owner, idBytes)
|
|
return
|
|
}
|
|
for _, id := range cdpIDs {
|
|
if id == cdp.ID {
|
|
return
|
|
}
|
|
cdpIDs = append(cdpIDs, cdp.ID)
|
|
store.Set(cdp.Owner, k.cdc.MustMarshalBinaryLengthPrefixed(cdpIDs))
|
|
}
|
|
}
|
|
|
|
// RemoveCdpOwnerIndex deletes the cdp id from the store's index of cdps by owner
|
|
func (k Keeper) RemoveCdpOwnerIndex(ctx sdk.Context, cdp types.CDP) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix)
|
|
cdpIDs, found := k.GetCdpIdsByOwner(ctx, cdp.Owner)
|
|
if !found {
|
|
return
|
|
}
|
|
updatedCdpIds := []uint64{}
|
|
for _, id := range cdpIDs {
|
|
if id != cdp.ID {
|
|
updatedCdpIds = append(updatedCdpIds, id)
|
|
}
|
|
}
|
|
if len(updatedCdpIds) == 0 {
|
|
store.Delete(cdp.Owner)
|
|
}
|
|
store.Set(cdp.Owner, k.cdc.MustMarshalBinaryLengthPrefixed(updatedCdpIds))
|
|
|
|
}
|
|
|
|
// IndexCdpByCollateralRatio sets the cdp id in the store, indexed by the collateral type and collateral to debt ratio
|
|
func (k Keeper) IndexCdpByCollateralRatio(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix)
|
|
db, _ := k.GetDenomPrefix(ctx, denom)
|
|
store.Set(types.CollateralRatioKey(db, id, collateralRatio), types.GetCdpIDBytes(id))
|
|
}
|
|
|
|
// RemoveCdpCollateralRatioIndex deletes the cdp id from the store's index of cdps by collateral type and collateral to debt ratio
|
|
func (k Keeper) RemoveCdpCollateralRatioIndex(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix)
|
|
db, _ := k.GetDenomPrefix(ctx, denom)
|
|
store.Delete(types.CollateralRatioKey(db, id, collateralRatio))
|
|
}
|
|
|
|
// GetDebtDenom returns the denom of debt in the system
|
|
func (k Keeper) GetDebtDenom(ctx sdk.Context) (denom string) {
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey)
|
|
bz := store.Get([]byte{})
|
|
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &denom)
|
|
return
|
|
}
|
|
|
|
// SetDebtDenom set the denom of debt in the system
|
|
func (k Keeper) SetDebtDenom(ctx sdk.Context, denom string) {
|
|
if denom == "" {
|
|
panic("debt denom not set in genesis")
|
|
}
|
|
store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey)
|
|
store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(denom))
|
|
return
|
|
}
|
|
|
|
// ValidateCollateral validates that a collateral is valid for use in cdps
|
|
func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) sdk.Error {
|
|
if len(collateral) != 1 {
|
|
return types.ErrInvalidCollateralLength(k.codespace, len(collateral))
|
|
}
|
|
_, found := k.GetCollateral(ctx, collateral[0].Denom)
|
|
if !found {
|
|
return types.ErrCollateralNotSupported(k.codespace, collateral[0].Denom)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
if !found {
|
|
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
|
|
}
|
|
if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) {
|
|
return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit)
|
|
}
|
|
if dc.Amount.LT(dp.DebtFloor) {
|
|
return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(dc), dp.DebtFloor)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 {
|
|
dp, found := k.GetDebt(ctx, dc.Denom)
|
|
if !found {
|
|
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
|
|
}
|
|
if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) {
|
|
return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateCollateralizationRatio validate that adding the input principal doesn't put the cdp below the liquidation ratio
|
|
func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) sdk.Error {
|
|
//
|
|
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, collateral, principal, fees)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom)
|
|
if collateralizationRatio.LT(liquidationRatio) {
|
|
return types.ErrInvalidCollateralRatio(k.codespace, collateral[0].Denom, collateralizationRatio, liquidationRatio)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CalculateCollateralToDebtRatio returns the collateral to debt ratio of the input collateral and debt amounts
|
|
func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.Coins, debt sdk.Coins) sdk.Dec {
|
|
debtTotal := sdk.ZeroDec()
|
|
for _, dc := range debt {
|
|
debtBaseUnits := k.convertDebtToBaseUnits(ctx, dc)
|
|
debtTotal = debtTotal.Add(debtBaseUnits)
|
|
}
|
|
|
|
if debtTotal.IsZero() || debtTotal.GTE(types.MaxSortableDec) {
|
|
return types.MaxSortableDec.Sub(sdk.SmallestDec())
|
|
}
|
|
|
|
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0])
|
|
return collateralBaseUnits.Quo(debtTotal)
|
|
}
|
|
|
|
// CalculateCollateralizationRatio returns the collateralization ratio of the input collateral to the input debt plus fees
|
|
func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, sdk.Error) {
|
|
marketID := k.getMarketID(ctx, collateral[0].Denom)
|
|
price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID)
|
|
if err != nil {
|
|
return sdk.Dec{}, err
|
|
}
|
|
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0])
|
|
collateralValue := collateralBaseUnits.Mul(price.Price)
|
|
principalTotal := sdk.ZeroDec()
|
|
for _, pc := range principal {
|
|
prinicpalBaseUnits := k.convertDebtToBaseUnits(ctx, pc)
|
|
principalTotal = principalTotal.Add(prinicpalBaseUnits)
|
|
}
|
|
for _, fc := range fees {
|
|
feeBaseUnits := k.convertDebtToBaseUnits(ctx, fc)
|
|
principalTotal = principalTotal.Add(feeBaseUnits)
|
|
}
|
|
collateralRatio := collateralValue.Quo(principalTotal)
|
|
return collateralRatio, nil
|
|
}
|
|
|
|
// converts the input collateral to base units (ie multiplies the input by 10^(-ConversionFactor))
|
|
func (k Keeper) convertCollateralToBaseUnits(ctx sdk.Context, collateral sdk.Coin) (baseUnits sdk.Dec) {
|
|
cp, _ := k.GetCollateral(ctx, collateral.Denom)
|
|
return sdk.NewDecFromInt(collateral.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), cp.ConversionFactor.Int64()))
|
|
}
|
|
|
|
// 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)
|
|
return sdk.NewDecFromInt(debt.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), dp.ConversionFactor.Int64()))
|
|
}
|