mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-27 08:45:19 +00:00
6ea518960a
* optimize cdp begin blocker by removing unnecessary checks, reusing data and prefix stores in loops, and reducing number of repeated calculations * fix panic for new cdp types if both previous accural time and global interest factor are not set * do not touch global interest factor if no CDP's exist; revert to panic if global interest factor is not found since this is an unreachable state by normal keeper operation -- it can only be reached if store is modified outside of public interface and normal operation
272 lines
9.7 KiB
Go
272 lines
9.7 KiB
Go
package keeper
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
sdkmath "cosmossdk.io/math"
|
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
|
|
"github.com/kava-labs/kava/x/cdp/types"
|
|
)
|
|
|
|
var scalingFactor = 1e18
|
|
|
|
// AccumulateInterest calculates the new interest that has accrued for the input collateral type based on the total amount of principal
|
|
// that has been created with that collateral type and the amount of time that has passed since interest was last accumulated
|
|
func (k Keeper) AccumulateInterest(ctx sdk.Context, ctype string) error {
|
|
previousAccrualTime, found := k.GetPreviousAccrualTime(ctx, ctype)
|
|
if !found {
|
|
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
|
|
return nil
|
|
}
|
|
|
|
timeElapsed := int64(math.RoundToEven(
|
|
ctx.BlockTime().Sub(previousAccrualTime).Seconds(),
|
|
))
|
|
if timeElapsed == 0 {
|
|
return nil
|
|
}
|
|
|
|
totalPrincipalPrior := k.GetTotalPrincipal(ctx, ctype, types.DefaultStableDenom)
|
|
if totalPrincipalPrior.IsZero() || totalPrincipalPrior.IsNegative() {
|
|
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
|
|
return nil
|
|
}
|
|
|
|
interestFactorPrior, foundInterestFactorPrior := k.GetInterestFactor(ctx, ctype)
|
|
if !foundInterestFactorPrior {
|
|
k.SetInterestFactor(ctx, ctype, sdk.OneDec())
|
|
// set previous accrual time exit early because interest accumulated will be zero
|
|
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
|
|
return nil
|
|
}
|
|
|
|
borrowRateSpy := k.getFeeRate(ctx, ctype)
|
|
if borrowRateSpy.Equal(sdk.OneDec()) {
|
|
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
|
|
return nil
|
|
}
|
|
interestFactor := CalculateInterestFactor(borrowRateSpy, sdkmath.NewInt(timeElapsed))
|
|
interestAccumulated := (interestFactor.Mul(sdk.NewDecFromInt(totalPrincipalPrior))).RoundInt().Sub(totalPrincipalPrior)
|
|
if interestAccumulated.IsZero() {
|
|
// in the case accumulated interest rounds to zero, exit early without updating accrual time
|
|
return nil
|
|
}
|
|
err := k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), sdk.NewCoin(types.DefaultStableDenom, interestAccumulated))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dp, found := k.GetDebtParam(ctx, types.DefaultStableDenom)
|
|
if !found {
|
|
panic(fmt.Sprintf("Debt parameters for %s not found", types.DefaultStableDenom))
|
|
}
|
|
|
|
newFeesSurplus := interestAccumulated
|
|
|
|
// mint surplus coins to the liquidator module account.
|
|
if newFeesSurplus.IsPositive() {
|
|
err := k.bankKeeper.MintCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSurplus)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
interestFactorNew := interestFactorPrior.Mul(interestFactor)
|
|
totalPrincipalNew := totalPrincipalPrior.Add(interestAccumulated)
|
|
|
|
k.SetTotalPrincipal(ctx, ctype, types.DefaultStableDenom, totalPrincipalNew)
|
|
k.SetInterestFactor(ctx, ctype, interestFactorNew)
|
|
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
|
|
|
|
return nil
|
|
}
|
|
|
|
// CalculateInterestFactor calculates the simple interest scaling factor,
|
|
// which is equal to: (per-second interest rate ** number of seconds elapsed)
|
|
// Will return 1.000x, multiply by principal to get new principal with added interest
|
|
func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdkmath.Int) sdk.Dec {
|
|
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
|
|
scalingFactorInt := sdkmath.NewInt(int64(scalingFactor))
|
|
|
|
// Convert per-second interest rate to a uint scaled by 1e18
|
|
interestMantissa := sdkmath.NewUintFromBigInt(perSecondInterestRate.MulInt(scalingFactorInt).RoundInt().BigInt())
|
|
|
|
// Convert seconds elapsed to uint (*not scaled*)
|
|
secondsElapsedUint := sdkmath.NewUintFromBigInt(secondsElapsed.BigInt())
|
|
|
|
// Calculate the interest factor as a uint scaled by 1e18
|
|
interestFactorMantissa := sdkmath.RelativePow(interestMantissa, secondsElapsedUint, scalingFactorUint)
|
|
|
|
// Convert interest factor to an unscaled sdk.Dec
|
|
return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
|
|
}
|
|
|
|
// SynchronizeInterest updates the input cdp object to reflect the current accumulated interest, updates the cdp state in the store,
|
|
// and returns the updated cdp object
|
|
func (k Keeper) SynchronizeInterest(ctx sdk.Context, cdp types.CDP) types.CDP {
|
|
globalInterestFactor, found := k.GetInterestFactor(ctx, cdp.Type)
|
|
if !found {
|
|
k.SetInterestFactor(ctx, cdp.Type, sdk.OneDec())
|
|
cdp.InterestFactor = sdk.OneDec()
|
|
cdp.FeesUpdated = ctx.BlockTime()
|
|
if err := k.SetCDP(ctx, cdp); err != nil {
|
|
panic(err)
|
|
}
|
|
return cdp
|
|
}
|
|
|
|
accumulatedInterest := k.CalculateNewInterest(ctx, cdp)
|
|
prevAccrualTime, found := k.GetPreviousAccrualTime(ctx, cdp.Type)
|
|
if !found {
|
|
return cdp
|
|
}
|
|
if accumulatedInterest.IsZero() {
|
|
// accumulated interest is zero if apy is zero or are if the total fees for all cdps round to zero
|
|
if cdp.FeesUpdated.Equal(prevAccrualTime) {
|
|
// if all fees are rounding to zero, don't update FeesUpdated
|
|
return cdp
|
|
}
|
|
// if apy is zero, we need to update FeesUpdated
|
|
cdp.FeesUpdated = prevAccrualTime
|
|
if err := k.SetCDP(ctx, cdp); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(accumulatedInterest)
|
|
cdp.FeesUpdated = prevAccrualTime
|
|
cdp.InterestFactor = globalInterestFactor
|
|
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
|
|
if err := k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return cdp
|
|
}
|
|
|
|
// CalculateNewInterest returns the amount of interest that has accrued to the cdp since its interest was last synchronized
|
|
func (k Keeper) CalculateNewInterest(ctx sdk.Context, cdp types.CDP) sdk.Coin {
|
|
globalInterestFactor, found := k.GetInterestFactor(ctx, cdp.Type)
|
|
if !found {
|
|
return sdk.NewCoin(cdp.AccumulatedFees.Denom, sdk.ZeroInt())
|
|
}
|
|
cdpInterestFactor := globalInterestFactor.Quo(cdp.InterestFactor)
|
|
if cdpInterestFactor.Equal(sdk.OneDec()) {
|
|
return sdk.NewCoin(cdp.AccumulatedFees.Denom, sdk.ZeroInt())
|
|
}
|
|
accumulatedInterest := sdk.NewDecFromInt(cdp.GetTotalPrincipal().Amount).Mul(cdpInterestFactor).RoundInt().Sub(cdp.GetTotalPrincipal().Amount)
|
|
return sdk.NewCoin(cdp.AccumulatedFees.Denom, accumulatedInterest)
|
|
}
|
|
|
|
// SynchronizeInterestForRiskyCDPs synchronizes the interest for the slice of cdps with the lowest collateral:debt ratio
|
|
func (k Keeper) SynchronizeInterestForRiskyCDPs(ctx sdk.Context, targetRatio sdk.Dec, cp types.CollateralParam) error {
|
|
debtParam := k.GetParams(ctx).DebtParam
|
|
|
|
cdpStore := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
|
|
collateralRatioStore := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix)
|
|
|
|
cdpIDs := make([]uint64, 0, cp.CheckCollateralizationIndexCount.Int64())
|
|
|
|
iterator := collateralRatioStore.Iterator(types.CollateralRatioIterKey(cp.Type, sdk.ZeroDec()), types.CollateralRatioIterKey(cp.Type, targetRatio))
|
|
for ; iterator.Valid(); iterator.Next() {
|
|
_, id, _ := types.SplitCollateralRatioKey(iterator.Key())
|
|
cdpIDs = append(cdpIDs, id)
|
|
if int64(len(cdpIDs)) >= cp.CheckCollateralizationIndexCount.Int64() {
|
|
break
|
|
}
|
|
}
|
|
iterator.Close()
|
|
|
|
globalInterestFactor, found := k.GetInterestFactor(ctx, cp.Type)
|
|
if !found && len(cdpIDs) > 0 {
|
|
panic(fmt.Sprintf("global interest factor not found for type %s", cp.Type))
|
|
}
|
|
prevAccrualTime, found := k.GetPreviousAccrualTime(ctx, cp.Type)
|
|
if !found {
|
|
panic(fmt.Sprintf("previous accrual time not found for type %s", cp.Type))
|
|
}
|
|
|
|
for _, cdpID := range cdpIDs {
|
|
//
|
|
// GET CDP
|
|
//
|
|
bz := cdpStore.Get(types.CdpKey(cp.Type, cdpID))
|
|
if bz == nil {
|
|
panic(fmt.Sprintf("cdp %d does not exist", cdpID))
|
|
}
|
|
var cdp types.CDP
|
|
k.cdc.MustUnmarshal(bz, &cdp)
|
|
|
|
if debtParam.Denom != cdp.GetTotalPrincipal().Denom {
|
|
panic(fmt.Sprintf("unkown debt param %s", cdp.GetTotalPrincipal().Denom))
|
|
}
|
|
|
|
//
|
|
// HOOK
|
|
//
|
|
k.hooks.BeforeCDPModified(ctx, cdp)
|
|
|
|
//
|
|
// CALC INTEREST
|
|
//
|
|
accumulatedInterest := sdk.ZeroInt()
|
|
cdpInterestFactor := globalInterestFactor.Quo(cdp.InterestFactor)
|
|
if !cdpInterestFactor.Equal(sdk.OneDec()) {
|
|
accumulatedInterest = sdk.NewDecFromInt(cdp.GetTotalPrincipal().Amount).Mul(cdpInterestFactor).RoundInt().Sub(cdp.GetTotalPrincipal().Amount)
|
|
}
|
|
|
|
if accumulatedInterest.IsZero() {
|
|
// accumulated interest is zero if apy is zero or are if the total fees for all cdps round to zero
|
|
if cdp.FeesUpdated.Equal(prevAccrualTime) {
|
|
// if all fees are rounding to zero, don't update FeesUpdated
|
|
continue
|
|
}
|
|
// if apy is zero, we need to update FeesUpdated
|
|
cdp.FeesUpdated = prevAccrualTime
|
|
bz = k.cdc.MustMarshal(&cdp)
|
|
cdpStore.Set(types.CdpKey(cdp.Type, cdp.ID), bz)
|
|
}
|
|
|
|
//
|
|
// GET OLD RATIO
|
|
//
|
|
previousCollateralRatio := calculateCollateralRatio(debtParam, cp, cdp)
|
|
|
|
//
|
|
// UPDATE CDP
|
|
//
|
|
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(sdk.NewCoin(cdp.AccumulatedFees.Denom, accumulatedInterest))
|
|
cdp.FeesUpdated = prevAccrualTime
|
|
cdp.InterestFactor = globalInterestFactor
|
|
|
|
//
|
|
// CALC NEW RATIO
|
|
//
|
|
updatedCollateralRatio := calculateCollateralRatio(debtParam, cp, cdp)
|
|
|
|
//
|
|
// UPDATE STORE
|
|
//
|
|
collateralRatioStore.Delete(types.CollateralRatioKey(cdp.Type, cdp.ID, previousCollateralRatio))
|
|
bz = k.cdc.MustMarshal(&cdp)
|
|
cdpStore.Set(types.CdpKey(cdp.Type, cdp.ID), bz)
|
|
collateralRatioStore.Set(types.CollateralRatioKey(cdp.Type, cdp.ID, updatedCollateralRatio), types.GetCdpIDBytes(cdp.ID))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func calculateCollateralRatio(debtParam types.DebtParam, collateralParam types.CollateralParam, cdp types.CDP) sdk.Dec {
|
|
debtTotal := sdk.NewDecFromInt(cdp.GetTotalPrincipal().Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), debtParam.ConversionFactor.Int64()))
|
|
|
|
if debtTotal.IsZero() || debtTotal.GTE(types.MaxSortableDec) {
|
|
return types.MaxSortableDec.Sub(sdk.SmallestDec())
|
|
} else {
|
|
collateralBaseUnits := sdk.NewDecFromInt(cdp.Collateral.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), collateralParam.ConversionFactor.Int64()))
|
|
return collateralBaseUnits.Quo(debtTotal)
|
|
}
|
|
}
|