package keeper import ( "fmt" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/cdp/types" ) // CalculateFees returns the fees accumulated since fees were last calculated based on // the input amount of outstanding debt (principal) and the number of periods (seconds) that have passed func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coin, periods sdk.Int, collateralType string) sdk.Coin { // how fees are calculated: // feesAccumulated = (outstandingDebt * (feeRate^periods)) - outstandingDebt // Note that since we can't do x^y using sdk.Decimal, we are converting to int and using RelativePow feePerSecond := k.getFeeRate(ctx, collateralType) scalar := sdk.NewInt(1000000000000000000) feeRateInt := feePerSecond.Mul(sdk.NewDecFromInt(scalar)).TruncateInt() accumulator := sdk.NewDecFromInt(types.RelativePow(feeRateInt, periods, scalar)).Mul(sdk.SmallestDec()) feesAccumulated := (sdk.NewDecFromInt(principal.Amount).Mul(accumulator)).Sub(sdk.NewDecFromInt(principal.Amount)) newFees := sdk.NewCoin(principal.Denom, feesAccumulated.TruncateInt()) return newFees } // UpdateFeesForAllCdps updates the fees for each of the CDPs func (k Keeper) UpdateFeesForAllCdps(ctx sdk.Context, collateralType string) error { var iterationErr error k.IterateCdpsByCollateralType(ctx, collateralType, func(cdp types.CDP) bool { oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal()) // periods = bblock timestamp - fees updated periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) newFees := k.CalculateFees(ctx, cdp.Principal, periods, collateralType) // exit without updating fees if amount has rounded down to zero // cdp will get updated next block when newFees, newFeesSavings, newFeesSurplus >0 if newFees.IsZero() { return false } dp, found := k.GetDebtParam(ctx, cdp.Principal.Denom) if !found { return false } savingsRate := dp.SavingsRate newFeesSavings := sdk.NewDecFromInt(newFees.Amount).Mul(savingsRate).RoundInt() newFeesSurplus := newFees.Amount.Sub(newFeesSavings) // similar to checking for rounding to zero of all fees, but in this case we // need to handle cases where we expect surplus or savings fees to be zero, namely // if newFeesSavings = 0, check if savings rate is not zero // if newFeesSurplus = 0, check if savings rate is not one if (newFeesSavings.IsZero() && !savingsRate.IsZero()) || (newFeesSurplus.IsZero() && !savingsRate.Equal(sdk.OneDec())) { return false } // mint debt coins to the cdp account err := k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees) if err != nil { iterationErr = err return true } previousDebt := k.GetTotalPrincipal(ctx, cdp.Type, dp.Denom) newDebt := previousDebt.Add(newFees.Amount) k.SetTotalPrincipal(ctx, cdp.Type, dp.Denom, newDebt) // mint surplus coins divided between the liquidator and savings module accounts. err = k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSurplus))) if err != nil { iterationErr = err return true } err = k.supplyKeeper.MintCoins(ctx, types.SavingsRateMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSavings))) if err != nil { iterationErr = err return true } // now add the new fees to the accumulated fees for the cdp cdp.AccumulatedFees = cdp.AccumulatedFees.Add(newFees) // and set the fees updated time to the current block time since we just updated it cdp.FeesUpdated = ctx.BlockTime() collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal()) k.RemoveCdpCollateralRatioIndex(ctx, cdp.Type, cdp.ID, oldCollateralToDebtRatio) err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) if err != nil { iterationErr = err return true } return false // this returns true when you want to stop iterating. Since we want to iterate through all we return false }) return iterationErr } // IncrementTotalPrincipal increments the total amount of debt that has been drawn with that collateral type func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralType string, principal sdk.Coin) { total := k.GetTotalPrincipal(ctx, collateralType, principal.Denom) total = total.Add(principal.Amount) k.SetTotalPrincipal(ctx, collateralType, principal.Denom, total) } // DecrementTotalPrincipal decrements the total amount of debt that has been drawn for a particular collateral type func (k Keeper) DecrementTotalPrincipal(ctx sdk.Context, collateralType string, principal sdk.Coin) { total := k.GetTotalPrincipal(ctx, collateralType, principal.Denom) // NOTE: negative total principal can happen in tests due to rounding errors // in fee calculation total = sdk.MaxInt(total.Sub(principal.Amount), sdk.ZeroInt()) k.SetTotalPrincipal(ctx, collateralType, principal.Denom, total) } // GetTotalPrincipal returns the total amount of principal that has been drawn for a particular collateral func (k Keeper) GetTotalPrincipal(ctx sdk.Context, collateralType, principalDenom string) (total sdk.Int) { store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix) bz := store.Get([]byte(collateralType + principalDenom)) if bz == nil { k.SetTotalPrincipal(ctx, collateralType, principalDenom, sdk.ZeroInt()) return sdk.ZeroInt() } k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &total) return total } // SetTotalPrincipal sets the total amount of principal that has been drawn for the input collateral func (k Keeper) SetTotalPrincipal(ctx sdk.Context, collateralType, principalDenom string, total sdk.Int) { store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix) _, found := k.GetCollateralTypePrefix(ctx, collateralType) if !found { panic(fmt.Sprintf("collateral not found: %s", collateralType)) } store.Set([]byte(collateralType+principalDenom), k.cdc.MustMarshalBinaryLengthPrefixed(total)) }