mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +00:00
Query Hard module's supply/borrow APYs (#816)
* calculate estimated apy from internal spy * implement interest rate query
This commit is contained in:
parent
802ed36846
commit
cd7a227030
@ -45,6 +45,7 @@ const (
|
||||
var (
|
||||
// function aliases
|
||||
APYToSPY = keeper.APYToSPY
|
||||
SPYToEstimatedAPY = keeper.SPYToEstimatedAPY
|
||||
CalculateBorrowInterestFactor = keeper.CalculateBorrowInterestFactor
|
||||
CalculateBorrowRate = keeper.CalculateBorrowRate
|
||||
CalculateSupplyInterestFactor = keeper.CalculateSupplyInterestFactor
|
||||
|
@ -41,6 +41,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
queryTotalDepositedCmd(queryRoute, cdc),
|
||||
queryBorrowsCmd(queryRoute, cdc),
|
||||
queryTotalBorrowedCmd(queryRoute, cdc),
|
||||
queryInterestRateCmd(queryRoute, cdc),
|
||||
)...)
|
||||
|
||||
return hardQueryCmd
|
||||
@ -316,3 +317,46 @@ func queryTotalDepositedCmd(queryRoute string, cdc *codec.Codec) *cobra.Command
|
||||
cmd.Flags().String(flagDenom, "", "(optional) filter total deposited coins by denom")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func queryInterestRateCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "interest-rate",
|
||||
Short: "get current money market interest rates",
|
||||
Long: strings.TrimSpace(`get current money market interest rates:
|
||||
|
||||
Example:
|
||||
$ kvcli q hard interest-rate
|
||||
$ kvcli q hard interest-rate --denom bnb`,
|
||||
),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
|
||||
denom := viper.GetString(flagDenom)
|
||||
|
||||
// Construct query with params
|
||||
params := types.NewQueryInterestRateParams(denom)
|
||||
bz, err := cdc.MarshalJSON(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Execute query
|
||||
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetInterestRate)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
|
||||
// Decode and print results
|
||||
var moneyMarketInterestRates types.MoneyMarketInterestRates
|
||||
if err := cdc.UnmarshalJSON(res, &moneyMarketInterestRates); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal money market interest rates: %w", err)
|
||||
}
|
||||
return cliCtx.PrintOutput(moneyMarketInterestRates)
|
||||
},
|
||||
}
|
||||
cmd.Flags().String(flagDenom, "", "(optional) filter interest rates by denom")
|
||||
return cmd
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||
r.HandleFunc(fmt.Sprintf("/%s/accounts", types.ModuleName), queryModAccountsHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/borrows", types.ModuleName), queryBorrowsHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/total-borrowed", types.ModuleName), queryTotalBorrowedHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/interest-rate", types.ModuleName), queryInterestRateHandlerFn(cliCtx)).Methods("GET")
|
||||
}
|
||||
|
||||
func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
@ -215,6 +216,44 @@ func queryTotalBorrowedHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func queryInterestRateHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _, _, err := rest.ParseHTTPArgsWithLimit(r, 0)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// Parse the query height
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var denom string
|
||||
|
||||
if x := r.URL.Query().Get(RestDenom); len(x) != 0 {
|
||||
denom = strings.TrimSpace(x)
|
||||
}
|
||||
|
||||
params := types.NewQueryInterestRateParams(denom)
|
||||
|
||||
bz, err := cliCtx.Codec.MarshalJSON(params)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetInterestRate)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
rest.PostProcessResponse(w, cliCtx, res)
|
||||
}
|
||||
}
|
||||
|
||||
func queryModAccountsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
|
||||
|
@ -300,6 +300,12 @@ func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// SPYToEstimatedAPY converts the internal per second compounded interest rate into an estimated annual
|
||||
// interest rate. The returned value is an estimate and should not be used for financial calculations.
|
||||
func SPYToEstimatedAPY(apy sdk.Dec) sdk.Dec {
|
||||
return apy.Power(uint64(secondsPerYear))
|
||||
}
|
||||
|
||||
// minInt64 returns the smaller of x or y
|
||||
func minDec(x, y sdk.Dec) sdk.Dec {
|
||||
if x.GT(y) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -454,6 +455,81 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *InterestTestSuite) TestSPYToEstimatedAPY() {
|
||||
type args struct {
|
||||
spy sdk.Dec
|
||||
expectedAPY float64
|
||||
acceptableRange float64
|
||||
}
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
args args
|
||||
}
|
||||
|
||||
testCases := []test{
|
||||
{
|
||||
"lowest apy",
|
||||
args{
|
||||
spy: sdk.MustNewDecFromStr("0.999999831991472557"),
|
||||
expectedAPY: 0.005, // Returned value: 0.004999999888241291
|
||||
acceptableRange: 0.00001, // +/- 1/10000th of a precent
|
||||
},
|
||||
},
|
||||
{
|
||||
"lower apy",
|
||||
args{
|
||||
spy: sdk.MustNewDecFromStr("0.999999905005957279"),
|
||||
expectedAPY: 0.05, // Returned value: 0.05000000074505806
|
||||
acceptableRange: 0.00001, // +/- 1/10000th of a precent
|
||||
},
|
||||
},
|
||||
{
|
||||
"medium-low apy",
|
||||
args{
|
||||
spy: sdk.MustNewDecFromStr("0.999999978020447332"),
|
||||
expectedAPY: 0.5, // Returned value: 0.5
|
||||
acceptableRange: 0.00001, // +/- 1/10000th of a precent
|
||||
},
|
||||
},
|
||||
{
|
||||
"medium-high apy",
|
||||
args{
|
||||
spy: sdk.MustNewDecFromStr("1.000000051034942717"),
|
||||
expectedAPY: 5, // Returned value: 5
|
||||
acceptableRange: 0.00001, // +/- 1/10000th of a precent
|
||||
},
|
||||
},
|
||||
{
|
||||
"high apy",
|
||||
args{
|
||||
spy: sdk.MustNewDecFromStr("1.000000124049443433"),
|
||||
expectedAPY: 50, // Returned value: 50
|
||||
acceptableRange: 0.00001, // +/- 1/10000th of a precent
|
||||
},
|
||||
},
|
||||
{
|
||||
"highest apy",
|
||||
args{
|
||||
spy: sdk.MustNewDecFromStr("1.000000146028999310"),
|
||||
expectedAPY: 100, // 100
|
||||
acceptableRange: 0.00001, // +/- 1/10000th of a precent
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// From SPY calculate APY and parse result from sdk.Dec to float64
|
||||
calculatedAPY := hard.SPYToEstimatedAPY(tc.args.spy)
|
||||
calculatedAPYFloat, err := strconv.ParseFloat(calculatedAPY.String(), 32)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check that the calculated value is within an acceptable percentage range
|
||||
suite.Require().InEpsilon(tc.args.expectedAPY, calculatedAPYFloat, tc.args.acceptableRange)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type ExpectedBorrowInterest struct {
|
||||
elapsedTime int64
|
||||
shouldBorrow bool
|
||||
|
@ -235,6 +235,15 @@ func (k Keeper) IterateMoneyMarkets(ctx sdk.Context, cb func(denom string, money
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllMoneyMarkets returns all money markets from the store
|
||||
func (k Keeper) GetAllMoneyMarkets(ctx sdk.Context) (moneyMarkets types.MoneyMarkets) {
|
||||
k.IterateMoneyMarkets(ctx, func(denom string, moneyMarket types.MoneyMarket) bool {
|
||||
moneyMarkets = append(moneyMarkets, moneyMarket)
|
||||
return false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// GetPreviousAccrualTime returns the last time an individual market accrued interest
|
||||
func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, denom string) (time.Time, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
|
||||
|
@ -28,6 +28,8 @@ func NewQuerier(k Keeper) sdk.Querier {
|
||||
return queryGetBorrows(ctx, req, k)
|
||||
case types.QueryGetTotalBorrowed:
|
||||
return queryGetTotalBorrowed(ctx, req, k)
|
||||
case types.QueryGetInterestRate:
|
||||
return queryGetInterestRate(ctx, req, k)
|
||||
default:
|
||||
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
|
||||
}
|
||||
@ -264,3 +266,65 @@ func queryGetTotalDeposited(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([
|
||||
|
||||
return bz, nil
|
||||
}
|
||||
|
||||
func queryGetInterestRate(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
var params types.QueryInterestRateParams
|
||||
err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||
}
|
||||
|
||||
var moneyMarketInterestRates types.MoneyMarketInterestRates
|
||||
var moneyMarkets types.MoneyMarkets
|
||||
if len(params.Denom) > 0 {
|
||||
moneyMarket, found := k.GetMoneyMarket(ctx, params.Denom)
|
||||
if !found {
|
||||
return nil, types.ErrMoneyMarketNotFound
|
||||
}
|
||||
moneyMarkets = append(moneyMarkets, moneyMarket)
|
||||
} else {
|
||||
moneyMarkets = k.GetAllMoneyMarkets(ctx)
|
||||
}
|
||||
|
||||
// Calculate the borrow and supply APY interest rates for each money market
|
||||
for _, moneyMarket := range moneyMarkets {
|
||||
denom := moneyMarket.Denom
|
||||
cash := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
|
||||
|
||||
borrowed := sdk.NewCoin(denom, sdk.ZeroInt())
|
||||
borrowedCoins, foundBorrowedCoins := k.GetBorrowedCoins(ctx)
|
||||
if foundBorrowedCoins {
|
||||
borrowed = sdk.NewCoin(denom, borrowedCoins.AmountOf(denom))
|
||||
}
|
||||
|
||||
reserves, foundReserves := k.GetTotalReserves(ctx)
|
||||
if !foundReserves {
|
||||
reserves = sdk.NewCoins()
|
||||
}
|
||||
|
||||
// CalculateBorrowRate calculates the current interest rate based on utilization (the fraction of supply that has been borrowed)
|
||||
borrowAPY, err := CalculateBorrowRate(moneyMarket.InterestRateModel, sdk.NewDecFromInt(cash), sdk.NewDecFromInt(borrowed.Amount), sdk.NewDecFromInt(reserves.AmountOf(denom)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
utilRatio := CalculateUtilizationRatio(sdk.NewDecFromInt(cash), sdk.NewDecFromInt(borrowed.Amount), sdk.NewDecFromInt(reserves.AmountOf(denom)))
|
||||
fullSupplyAPY := borrowAPY.Mul(utilRatio)
|
||||
realSupplyAPY := fullSupplyAPY.Mul(sdk.OneDec().Sub(moneyMarket.ReserveFactor))
|
||||
|
||||
moneyMarketInterestRate := types.MoneyMarketInterestRate{
|
||||
Denom: denom,
|
||||
SupplyInterestRate: realSupplyAPY,
|
||||
BorrowInterestRate: borrowAPY,
|
||||
}
|
||||
|
||||
moneyMarketInterestRates = append(moneyMarketInterestRates, moneyMarketInterestRate)
|
||||
}
|
||||
|
||||
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, moneyMarketInterestRates)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
|
||||
return bz, nil
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ const (
|
||||
QueryGetTotalDeposited = "total-deposited"
|
||||
QueryGetBorrows = "borrows"
|
||||
QueryGetTotalBorrowed = "total-borrowed"
|
||||
QueryGetInterestRate = "interest-rate"
|
||||
)
|
||||
|
||||
// QueryDepositsParams is the params for a filtered deposit query
|
||||
@ -89,3 +90,34 @@ func NewQueryTotalDepositedParams(denom string) QueryTotalDepositedParams {
|
||||
Denom: denom,
|
||||
}
|
||||
}
|
||||
|
||||
// QueryInterestRateParams is the params for a filtered interest rate query
|
||||
type QueryInterestRateParams struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
}
|
||||
|
||||
// NewQueryInterestRateParams creates a new QueryInterestRateParams
|
||||
func NewQueryInterestRateParams(denom string) QueryInterestRateParams {
|
||||
return QueryInterestRateParams{
|
||||
Denom: denom,
|
||||
}
|
||||
}
|
||||
|
||||
// MoneyMarketInterestRate is a unique type returned by interest rate queries
|
||||
type MoneyMarketInterestRate struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
SupplyInterestRate sdk.Dec `json:"supply_interest_rate" yaml:"supply_interest_rate"`
|
||||
BorrowInterestRate sdk.Dec `json:"borrow_interest_rate" yaml:"borrow_interest_rate"`
|
||||
}
|
||||
|
||||
// NewMoneyMarketInterestRate returns a new instance of MoneyMarketInterestRate
|
||||
func NewMoneyMarketInterestRate(denom string, supplyInterestRate, borrowInterestRate sdk.Dec) MoneyMarketInterestRate {
|
||||
return MoneyMarketInterestRate{
|
||||
Denom: denom,
|
||||
SupplyInterestRate: supplyInterestRate,
|
||||
BorrowInterestRate: borrowInterestRate,
|
||||
}
|
||||
}
|
||||
|
||||
// MoneyMarketInterestRates is a slice of MoneyMarketInterestRate
|
||||
type MoneyMarketInterestRates []MoneyMarketInterestRate
|
||||
|
Loading…
Reference in New Issue
Block a user