mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 15:55: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 (
|
var (
|
||||||
// function aliases
|
// function aliases
|
||||||
APYToSPY = keeper.APYToSPY
|
APYToSPY = keeper.APYToSPY
|
||||||
|
SPYToEstimatedAPY = keeper.SPYToEstimatedAPY
|
||||||
CalculateBorrowInterestFactor = keeper.CalculateBorrowInterestFactor
|
CalculateBorrowInterestFactor = keeper.CalculateBorrowInterestFactor
|
||||||
CalculateBorrowRate = keeper.CalculateBorrowRate
|
CalculateBorrowRate = keeper.CalculateBorrowRate
|
||||||
CalculateSupplyInterestFactor = keeper.CalculateSupplyInterestFactor
|
CalculateSupplyInterestFactor = keeper.CalculateSupplyInterestFactor
|
||||||
|
@ -41,6 +41,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
|||||||
queryTotalDepositedCmd(queryRoute, cdc),
|
queryTotalDepositedCmd(queryRoute, cdc),
|
||||||
queryBorrowsCmd(queryRoute, cdc),
|
queryBorrowsCmd(queryRoute, cdc),
|
||||||
queryTotalBorrowedCmd(queryRoute, cdc),
|
queryTotalBorrowedCmd(queryRoute, cdc),
|
||||||
|
queryInterestRateCmd(queryRoute, cdc),
|
||||||
)...)
|
)...)
|
||||||
|
|
||||||
return hardQueryCmd
|
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")
|
cmd.Flags().String(flagDenom, "", "(optional) filter total deposited coins by denom")
|
||||||
return cmd
|
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/accounts", types.ModuleName), queryModAccountsHandlerFn(cliCtx)).Methods("GET")
|
||||||
r.HandleFunc(fmt.Sprintf("/%s/borrows", types.ModuleName), queryBorrowsHandlerFn(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/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 {
|
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 {
|
func queryModAccountsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
|
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
|
||||||
|
@ -300,6 +300,12 @@ func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
|
|||||||
return root, nil
|
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
|
// minInt64 returns the smaller of x or y
|
||||||
func minDec(x, y sdk.Dec) sdk.Dec {
|
func minDec(x, y sdk.Dec) sdk.Dec {
|
||||||
if x.GT(y) {
|
if x.GT(y) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package keeper_test
|
package keeper_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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 {
|
type ExpectedBorrowInterest struct {
|
||||||
elapsedTime int64
|
elapsedTime int64
|
||||||
shouldBorrow bool
|
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
|
// GetPreviousAccrualTime returns the last time an individual market accrued interest
|
||||||
func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, denom string) (time.Time, bool) {
|
func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, denom string) (time.Time, bool) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
|
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
|
||||||
|
@ -28,6 +28,8 @@ func NewQuerier(k Keeper) sdk.Querier {
|
|||||||
return queryGetBorrows(ctx, req, k)
|
return queryGetBorrows(ctx, req, k)
|
||||||
case types.QueryGetTotalBorrowed:
|
case types.QueryGetTotalBorrowed:
|
||||||
return queryGetTotalBorrowed(ctx, req, k)
|
return queryGetTotalBorrowed(ctx, req, k)
|
||||||
|
case types.QueryGetInterestRate:
|
||||||
|
return queryGetInterestRate(ctx, req, k)
|
||||||
default:
|
default:
|
||||||
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
|
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
|
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"
|
QueryGetTotalDeposited = "total-deposited"
|
||||||
QueryGetBorrows = "borrows"
|
QueryGetBorrows = "borrows"
|
||||||
QueryGetTotalBorrowed = "total-borrowed"
|
QueryGetTotalBorrowed = "total-borrowed"
|
||||||
|
QueryGetInterestRate = "interest-rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QueryDepositsParams is the params for a filtered deposit query
|
// QueryDepositsParams is the params for a filtered deposit query
|
||||||
@ -89,3 +90,34 @@ func NewQueryTotalDepositedParams(denom string) QueryTotalDepositedParams {
|
|||||||
Denom: denom,
|
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