0g-chain/x/harvest/keeper/interest.go

194 lines
6.8 KiB
Go
Raw Normal View History

package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/harvest/types"
)
var (
scalingFactor = 1e18
secondsPerYear = 31536000
)
// ApplyInterestRateUpdates translates the current interest rate models from the params to the store,
// with each money market accruing interest.
func (k Keeper) ApplyInterestRateUpdates(ctx sdk.Context) {
denomSet := map[string]bool{}
params := k.GetParams(ctx)
for _, mm := range params.MoneyMarkets {
// Set any new money markets in the store
moneyMarket, found := k.GetMoneyMarket(ctx, mm.Denom)
if !found {
moneyMarket = mm
k.SetMoneyMarket(ctx, mm.Denom, moneyMarket)
}
// Accrue interest according to the current money markets in the store
err := k.AccrueInterest(ctx, mm.Denom)
if err != nil {
panic(err)
}
// Update the interest rate in the store if the params have changed
if !moneyMarket.Equal(mm) {
k.SetMoneyMarket(ctx, mm.Denom, mm)
}
denomSet[mm.Denom] = true
}
// Edge case: money markets removed from params that still exist in the store
k.IterateMoneyMarkets(ctx, func(denom string, i types.MoneyMarket) bool {
if !denomSet[denom] {
// Accrue interest according to current store money market
err := k.AccrueInterest(ctx, denom)
if err != nil {
panic(err)
}
// Delete the money market from the store
k.DeleteMoneyMarket(ctx, denom)
}
return false
})
}
// AccrueInterest applies accrued interest to total borrows and reserves by calculating
// interest from the last checkpoint time and writing the updated values to the store.
func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
previousAccrualTime, found := k.GetPreviousAccrualTime(ctx, denom)
if !found {
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
timeElapsed := ctx.BlockTime().Unix() - previousAccrualTime.Unix()
if timeElapsed == 0 {
return nil
}
// Get available harvest module account cash on hand
cashPrior := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
// Get prior borrows
borrowsPrior := sdk.NewCoin(denom, sdk.ZeroInt())
borrowCoinsPrior, foundBorrowCoinsPrior := k.GetBorrowedCoins(ctx)
if foundBorrowCoinsPrior {
borrowsPrior = sdk.NewCoin(denom, borrowCoinsPrior.AmountOf(denom))
}
reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx, denom)
if !foundReservesPrior {
newReservesPrior := sdk.NewCoin(denom, sdk.ZeroInt())
k.SetTotalReserves(ctx, denom, newReservesPrior)
reservesPrior = newReservesPrior
}
borrowIndexPrior, foundBorrowIndexPrior := k.GetBorrowIndex(ctx, denom)
if !foundBorrowIndexPrior {
newBorrowIndexPrior := sdk.MustNewDecFromStr("1.0")
k.SetBorrowIndex(ctx, denom, newBorrowIndexPrior)
borrowIndexPrior = newBorrowIndexPrior
}
// Fetch money market from the store
mm, found := k.GetMoneyMarket(ctx, denom)
if !found {
return sdkerrors.Wrapf(types.ErrMoneyMarketNotFound, "%s", denom)
}
// GetBorrowRate calculates the current interest rate based on utilization (the fraction of supply that has been borrowed)
borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowsPrior.Amount), sdk.NewDecFromInt(reservesPrior.Amount))
if err != nil {
return err
}
// Convert from APY to SPY, expressed as (1 + borrow rate)
borrowRateSpy, err := APYToSPY(sdk.OneDec().Add(borrowRateApy))
if err != nil {
return err
}
interestFactor := CalculateInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
interestAccumulated := (interestFactor.Mul(sdk.NewDecFromInt(borrowsPrior.Amount)).TruncateInt()).Sub(borrowsPrior.Amount)
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestAccumulated))
totalReservesNew := reservesPrior.Add(sdk.NewCoin(denom, sdk.NewDecFromInt(interestAccumulated).Mul(mm.ReserveFactor).TruncateInt()))
borrowIndexNew := borrowIndexPrior.Mul(interestFactor)
k.SetBorrowIndex(ctx, denom, borrowIndexNew)
k.IncrementBorrowedCoins(ctx, totalBorrowInterestAccumulated)
k.SetTotalReserves(ctx, denom, totalReservesNew)
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
return nil
}
// CalculateBorrowRate calculates the borrow rate, which is the current APY expressed as a decimal
// based on the current utilization.
func CalculateBorrowRate(model types.InterestRateModel, cash, borrows, reserves sdk.Dec) (sdk.Dec, error) {
utilRatio := CalculateUtilizationRatio(cash, borrows, reserves)
// Calculate normal borrow rate (under kink)
if utilRatio.LTE(model.Kink) {
return utilRatio.Mul(model.BaseMultiplier).Add(model.BaseRateAPY), nil
}
// Calculate jump borrow rate (over kink)
normalRate := model.Kink.Mul(model.BaseMultiplier).Add(model.BaseRateAPY)
excessUtil := utilRatio.Sub(model.Kink)
return excessUtil.Mul(model.JumpMultiplier).Add(normalRate), nil
}
// CalculateUtilizationRatio calculates an asset's current utilization rate
func CalculateUtilizationRatio(cash, borrows, reserves sdk.Dec) sdk.Dec {
// Utilization rate is 0 when there are no borrows
if borrows.Equal(sdk.ZeroDec()) {
return sdk.ZeroDec()
}
totalSupply := cash.Add(borrows).Sub(reserves)
if totalSupply.IsNegative() {
return sdk.OneDec()
}
return sdk.MinDec(sdk.OneDec(), borrows.Quo(totalSupply))
}
// 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 sdk.Int) sdk.Dec {
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
scalingFactorInt := sdk.NewInt(int64(scalingFactor))
// Convert per-second interest rate to a uint scaled by 1e18
interestMantissa := sdk.NewUint(perSecondInterestRate.MulInt(scalingFactorInt).RoundInt().Uint64())
// Convert seconds elapsed to uint (*not scaled*)
secondsElapsedUint := sdk.NewUint(secondsElapsed.Uint64())
// Calculate the interest factor as a uint scaled by 1e18
interestFactorMantissa := sdk.RelativePow(interestMantissa, secondsElapsedUint, scalingFactorUint)
// Convert interest factor to an unscaled sdk.Dec
return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
}
// APYToSPY converts the input annual interest rate. For example, 10% apy would be passed as 1.10.
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
// Note: any APY 179 or greater will cause an out-of-bounds error
root, err := apy.ApproxRoot(uint64(secondsPerYear))
if err != nil {
return sdk.ZeroDec(), err
}
return root, nil
}
// minInt64 returns the smaller of x or y
func minDec(x, y sdk.Dec) sdk.Dec {
if x.GT(y) {
return y
}
return x
}