Query Hard module's supply/borrow APYs (#816)

* calculate estimated apy from internal spy

* implement interest rate query
This commit is contained in:
Denali Marsh 2021-02-10 18:53:53 +01:00 committed by GitHub
parent 802ed36846
commit cd7a227030
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 271 additions and 0 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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) {

View File

@ -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

View File

@ -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)

View File

@ -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, &params)
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
}

View File

@ -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