Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions proto/oracle/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,13 @@ message PriceSnapshot {
(gogoproto.nullable) = false
];
}

message OracleTwap {
string denom = 1;
string twap = 2 [
(gogoproto.moretags) = "yaml:\"twap\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
int64 lookbackSeconds = 3;
}
16 changes: 16 additions & 0 deletions proto/oracle/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ service Query {
option (google.api.http).get = "/sei-protocol/sei-chain/oracle/denoms/price_snapshot_history";
}

rpc Twaps(QueryTwapsRequest) returns (QueryTwapsResponse) {
option (google.api.http).get = "/sei-protocol/sei-chain/oracle/denoms/twaps";
}

// FeederDelegation returns feeder delegation of a validator
rpc FeederDelegation(QueryFeederDelegationRequest) returns (QueryFeederDelegationResponse) {
option (google.api.http).get = "/sei-protocol/sei-chain/oracle/validators/{validator_addr}/feeder";
Expand Down Expand Up @@ -133,6 +137,18 @@ message QueryPriceSnapshotHistoryResponse {
];
}

// request type for twap RPC method
message QueryTwapsRequest {
int64 lookback_seconds = 1;
}

message QueryTwapsResponse {
repeated OracleTwap oracle_twaps = 1 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "OracleTwaps"
];
}

// QueryFeederDelegationRequest is the request type for the Query/FeederDelegation RPC method.
message QueryFeederDelegationRequest {
option (gogoproto.equal) = false;
Expand Down
10 changes: 6 additions & 4 deletions x/oracle/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) {
priceSnapshotItems = append(priceSnapshotItems, priceSnapshotItem)
return false
})
priceSnapshot := types.PriceSnapshot{
SnapshotTimestamp: ctx.BlockTime().Unix(),
PriceSnapshotItems: priceSnapshotItems,
if len(priceSnapshotItems) > 0 {
priceSnapshot := types.PriceSnapshot{
SnapshotTimestamp: ctx.BlockTime().Unix(),
PriceSnapshotItems: priceSnapshotItems,
}
k.AddPriceSnapshot(ctx, priceSnapshot)
}
k.AddPriceSnapshot(ctx, priceSnapshot)
}

// Do slash who did miss voting over threshold and
Expand Down
41 changes: 41 additions & 0 deletions x/oracle/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"context"
"strconv"
"strings"

"github.com/spf13/cobra"
Expand All @@ -26,6 +27,7 @@ func GetQueryCmd() *cobra.Command {
oracleQueryCmd.AddCommand(
GetCmdQueryExchangeRates(),
GetCmdQueryPriceSnapshotHistory(),
GetCmdQueryTwaps(),
GetCmdQueryActives(),
GetCmdQueryParams(),
GetCmdQueryFeederDelegation(),
Expand Down Expand Up @@ -120,6 +122,45 @@ $ seid query oracle price-snapshot-history
return cmd
}

func GetCmdQueryTwaps() *cobra.Command {
cmd := &cobra.Command{
Use: "twaps [lookback-seconds]",
Args: cobra.ExactArgs(1),
Short: "Query the time weighted average prices for denoms with price snapshot data",
Long: strings.TrimSpace(`
Query the time weighted average prices for denoms with price snapshot data
Example:

$ seid query oracle twaps
`),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)

lookbackSeconds, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return err
}

res, err := queryClient.Twaps(
context.Background(),
&types.QueryTwapsRequest{LookbackSeconds: lookbackSeconds},
)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)
return cmd
}

// GetCmdQueryActives implements the query actives command.
func GetCmdQueryActives() *cobra.Command {
cmd := &cobra.Command{
Expand Down
42 changes: 42 additions & 0 deletions x/oracle/client/rest/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rest
import (
"fmt"
"net/http"
"strconv"

"github.com/gorilla/mux"

Expand All @@ -18,6 +19,7 @@ func registerQueryRoutes(cliCtx client.Context, rtr *mux.Router) {
rtr.HandleFunc("/oracle/denoms/price_snapshot_history", queryPriceSnapshotHistoryHandlerFunction(cliCtx)).Methods("GET")
rtr.HandleFunc("/oracle/denoms/actives", queryActivesHandlerFunction(cliCtx)).Methods("GET")
rtr.HandleFunc("/oracle/denoms/exchange_rates", queryExchangeRatesHandlerFunction(cliCtx)).Methods("GET")
rtr.HandleFunc("/oracle/denoms/twaps", queryTwapsHandlerFunction(cliCtx)).Methods("GET")
rtr.HandleFunc("/oracle/denoms/vote_targets", queryVoteTargetsHandlerFunction(cliCtx)).Methods("GET")
rtr.HandleFunc(fmt.Sprintf("/oracle/voters/{%s}/feeder", RestVoter), queryFeederDelegationHandlerFunction(cliCtx)).Methods("GET")
rtr.HandleFunc(fmt.Sprintf("/oracle/voters/{%s}/miss", RestVoter), queryMissHandlerFunction(cliCtx)).Methods("GET")
Expand Down Expand Up @@ -89,6 +91,34 @@ func queryPriceSnapshotHistoryHandlerFunction(cliCtx client.Context) http.Handle
}
}

func queryTwapsHandlerFunction(cliCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}

lookbackSeconds, ok := checkLookbackSecondsVar(w, r)
if !ok {
return
}

params := types.NewQueryTwapsParams(lookbackSeconds)
bz, err := cliCtx.LegacyAmino.MarshalJSON(params)
if rest.CheckBadRequestError(w, err) {
return
}

res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryTwaps), bz)
if rest.CheckInternalServerError(w, err) {
return
}

cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}

func queryActivesHandlerFunction(cliCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
Expand Down Expand Up @@ -292,6 +322,18 @@ func checkDenomVar(w http.ResponseWriter, r *http.Request) (string, bool) {
return denom, true
}

func checkLookbackSecondsVar(w http.ResponseWriter, r *http.Request) (int64, bool) {
lookbackSecondsStr := mux.Vars(r)[RestLookbackSeconds]
lookbackSeconds, err := strconv.ParseInt(lookbackSecondsStr, 10, 64)
if err != nil {
return 0, false
}
if lookbackSeconds <= 0 {
return 0, false
}
return lookbackSeconds, true
}

func checkVoterAddressVar(w http.ResponseWriter, r *http.Request) (sdk.ValAddress, bool) {
addr, err := sdk.ValAddressFromBech32(mux.Vars(r)[RestVoter])
if rest.CheckBadRequestError(w, err) {
Expand Down
5 changes: 3 additions & 2 deletions x/oracle/client/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (

//nolint
const (
RestDenom = "denom"
RestVoter = "voter"
RestDenom = "denom"
RestVoter = "voter"
RestLookbackSeconds = "lookback_seconds"
)

// RegisterRoutes registers oracle-related REST handlers to a router
Expand Down
104 changes: 104 additions & 0 deletions x/oracle/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"fmt"
"sort"

"github.com/tendermint/tendermint/libs/log"

Expand Down Expand Up @@ -417,7 +418,110 @@ func (k Keeper) IteratePriceSnapshots(ctx sdk.Context, handler func(snapshot typ
}
}

func (k Keeper) IteratePriceSnapshotsReverse(ctx sdk.Context, handler func(snapshot types.PriceSnapshot) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStoreReversePrefixIterator(store, types.PriceSnapshotKey)
defer iterator.Close()

for ; iterator.Valid(); iterator.Next() {
var val types.PriceSnapshot
k.cdc.MustUnmarshal(iterator.Value(), &val)
if handler(val) {
break
}
}
}

func (k Keeper) DeletePriceSnapshot(ctx sdk.Context, timestamp int64) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.GetPriceSnapshotKey(uint64(timestamp)))
}

func (k Keeper) CalculateTwaps(ctx sdk.Context, lookbackSeconds int64) (types.OracleTwaps, error) {
oracleTwaps := types.OracleTwaps{}
currentTime := ctx.BlockTime().Unix()
err := k.ValidateLookbackSeconds(ctx, lookbackSeconds)
if err != nil {
return oracleTwaps, err
}
var timeTraversed int64 = 0
denomToTimeWeightedMap := make(map[string]sdk.Dec)
denomDurationMap := make(map[string]int64)

k.IteratePriceSnapshotsReverse(ctx, func(snapshot types.PriceSnapshot) (stop bool) {
stop = false
snapshotTimestamp := snapshot.SnapshotTimestamp
if currentTime-lookbackSeconds > snapshotTimestamp {
snapshotTimestamp = currentTime - lookbackSeconds
stop = true
}
// update time traversed to represent current snapshot
// replace SnapshotTimestamp with lookback duration bounding
timeTraversed = currentTime - snapshotTimestamp

// iterate through denoms in the snapshot
// if we find a new one, we have to setup the TWAP calc for that one
snapshotPriceItems := snapshot.PriceSnapshotItems
for _, priceItem := range snapshotPriceItems {
denom := priceItem.Denom

_, exists := denomToTimeWeightedMap[denom]
if !exists {
// set up the TWAP info for a denom
denomToTimeWeightedMap[denom] = sdk.ZeroDec()
denomDurationMap[denom] = 0
}
// get the denom specific TWAP data
denomTimeWeightedSum := denomToTimeWeightedMap[denom]
denomDuration := denomDurationMap[denom]

// calculate the new Time Weighted Sum for the denom exchange rate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mind adding a comment here including the twap formula?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup will do

// we calculate a weighted sum of exchange rates previously by multiplying each exchange rate by time interval that it was active
// then we divide by the overall time in the lookback window, which gives us the time weighted average
durationDifference := timeTraversed - denomDuration
exchangeRate := priceItem.OracleExchangeRate.ExchangeRate
denomTimeWeightedSum = denomTimeWeightedSum.Add(exchangeRate.MulInt64(durationDifference))

// set the denom TWAP data
denomToTimeWeightedMap[denom] = denomTimeWeightedSum
denomDurationMap[denom] = timeTraversed
}
return
})

denomKeys := make([]string, 0, len(denomToTimeWeightedMap))
for k := range denomToTimeWeightedMap {
denomKeys = append(denomKeys, k)
}
sort.Strings(denomKeys)

// iterate over all denoms with TWAP data
for _, denomKey := range denomKeys {
// divide the denom time weighed sum by denom duration
denomTimeWeightedSum := denomToTimeWeightedMap[denomKey]
denomDuration := denomDurationMap[denomKey]
denomTwap := denomTimeWeightedSum.QuoInt64(denomDuration)

denomOracleTwap := types.OracleTwap{
Denom: denomKey,
Twap: denomTwap,
LookbackSeconds: denomDuration,
}
oracleTwaps = append(oracleTwaps, denomOracleTwap)
}

if len(oracleTwaps) == 0 {
return oracleTwaps, types.ErrNoTwapData
}

return oracleTwaps, nil
}

func (k Keeper) ValidateLookbackSeconds(ctx sdk.Context, lookbackSeconds int64) error {
lookbackDuration := k.LookbackDuration(ctx)
if lookbackSeconds > lookbackDuration || lookbackSeconds <= 0 {
return types.ErrInvalidTwapLookback
}

return nil
}
Loading