Skip to content

Commit

Permalink
feat(oracle): add timestamp to exchange rates (#2254)
Browse files Browse the repository at this point in the history
* add timestamp to denom price

* address the first review comments

* add SetExchangeRateWithTimestamp func

* storing rate and timestamp without denom into exg rates

* renamed ExchangeRate to DenomExchangeRate on oracle genesis
 and using ExchangeRate for return types on oracle

* address the review comments

* add some tests and cli query for getting timestamps of exgrates

* add tests to cli and grpc exg rates with timestamp req

* fix the tests on oracle

* fix the tests

---------

Co-authored-by: Adam Moser <[email protected]>
  • Loading branch information
gsk967 and toteki authored Sep 27, 2023
1 parent bd3e455 commit 498bd30
Show file tree
Hide file tree
Showing 27 changed files with 1,118 additions and 327 deletions.
5 changes: 2 additions & 3 deletions app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,8 @@ func IntegrationTestNetworkConfig() network.Config {
// execute ballot voting and thus clear out previous exchange rates, since we
// are not running a price-feeder.
oracleGenState.Params.VotePeriod = 1000
oracleGenState.ExchangeRates = append(oracleGenState.ExchangeRates, oracletypes.NewExchangeRateTuple(
params.DisplayDenom, sdk.MustNewDecFromStr("34.21"),
))
oracleGenState.ExchangeRates = append(oracleGenState.ExchangeRates, oracletypes.NewDenomExchangeRate(
params.DisplayDenom, sdk.MustNewDecFromStr("34.21"), time.Now()))
// Set mock historic medians to satisfy leverage module's 24 median requirement
for i := 1; i <= 24; i++ {
median := oracletypes.Price{
Expand Down
23 changes: 8 additions & 15 deletions proto/umee/oracle/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,15 @@ option (gogoproto.goproto_getters_all) = false;

// GenesisState defines the oracle module's genesis state.
message GenesisState {
Params params = 1 [(gogoproto.nullable) = false];
repeated FeederDelegation feeder_delegations = 2
[(gogoproto.nullable) = false];
// TODO: need to update this to save data with timestamp
repeated ExchangeRateTuple exchange_rates = 3 [
(gogoproto.castrepeated) = "ExchangeRateTuples",
(gogoproto.nullable) = false
];
Params params = 1 [(gogoproto.nullable) = false];
repeated FeederDelegation feeder_delegations = 2 [(gogoproto.nullable) = false];
repeated DenomExchangeRate exchange_rates = 3 [(gogoproto.nullable) = false];
repeated MissCounter miss_counters = 4 [(gogoproto.nullable) = false];
repeated AggregateExchangeRatePrevote aggregate_exchange_rate_prevotes = 5
[(gogoproto.nullable) = false];
repeated AggregateExchangeRateVote aggregate_exchange_rate_votes = 6
[(gogoproto.nullable) = false];
repeated Price medians = 7 [(gogoproto.nullable) = false];
repeated Price historic_prices = 8 [(gogoproto.nullable) = false];
repeated Price medianDeviations = 9 [(gogoproto.nullable) = false];
repeated AggregateExchangeRatePrevote aggregate_exchange_rate_prevotes = 5 [(gogoproto.nullable) = false];
repeated AggregateExchangeRateVote aggregate_exchange_rate_votes = 6 [(gogoproto.nullable) = false];
repeated Price medians = 7 [(gogoproto.nullable) = false];
repeated Price historic_prices = 8 [(gogoproto.nullable) = false];
repeated Price medianDeviations = 9 [(gogoproto.nullable) = false];
// Historic Avg Counter params
AvgCounterParams avg_counter_params = 10 [
(gogoproto.moretags) = "yaml:\"avg_counter_params\"",
Expand Down
8 changes: 4 additions & 4 deletions proto/umee/oracle/v1/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ message AvgCounter {
google.protobuf.Timestamp start = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
}

// ExchangeRate stores exchange rate with timestamp
message ExchangeRate {
// DenomExchangeRate stores exchange rate with timestamp
message DenomExchangeRate {
option (gogoproto.equal) = false;
option (gogoproto.goproto_stringer) = false;

string rate = 1 [
string denom = 1 [(gogoproto.moretags) = "yaml:\"denom\""];
string rate = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
Expand Down
28 changes: 28 additions & 0 deletions proto/umee/oracle/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,34 @@ service Query {
option (google.api.http).get =
"/umee/historacle/v1/avg_price/{denom}";
}

// ExgRatesWithTimestamp returns exchange rates of all denoms with timestamp,
// or, if specified, returns a single denom
rpc ExgRatesWithTimestamp(QueryExgRatesWithTimestamp)
returns (QueryExgRatesWithTimestampResponse) {
option (google.api.http).get =
"/umee/oracle/v1/denoms/exg_rates_timestamp";
}
}

// QueryExgRatesWithTimestamp is the request type for the Query/ExchangeRatesWithTimestamp RPC
// method.
message QueryExgRatesWithTimestamp {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

// denom defines the denomination to query for.
string denom = 1;
}

// QueryExgRatesWithTimestampResponse is response type for the
// Query/ExchangeRatesWithTimestamp RPC method.
message QueryExgRatesWithTimestampResponse {
// exchange_rates defines a list of the exchange rate for all whitelisted
// denoms with timestamp
repeated DenomExchangeRate exg_rates = 1 [
(gogoproto.nullable) = false
];
}

// QueryExchangeRates is the request type for the Query/ExchangeRate RPC
Expand Down
4 changes: 2 additions & 2 deletions x/leverage/simulation/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package simulation_test
import (
"math/rand"
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
Expand Down Expand Up @@ -32,8 +33,7 @@ type SimTestSuite struct {
func (s *SimTestSuite) SetupTest() {
checkTx := false
app := umeeapp.Setup(s.T())
ctx := app.NewContext(checkTx, tmproto.Header{})

ctx := app.NewContext(checkTx, tmproto.Header{Time: time.Now()})
leverage.InitGenesis(ctx, app.LeverageKeeper, *types.DefaultGenesis())

// Use default umee token for sim tests
Expand Down
1 change: 1 addition & 0 deletions x/oracle/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func CalcPrices(ctx sdk.Context, params types.Params, k keeper.Keeper) error {
if err != nil {
return err
}
// save the exchange rate to store with denom and timestamp
k.SetExchangeRateWithEvent(ctx, denom, exchangeRate)

if k.IsPeriodLastBlock(ctx, params.HistoricStampPeriod) {
Expand Down
10 changes: 7 additions & 3 deletions x/oracle/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package oracle_test
import (
"fmt"
"testing"
"time"

"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -43,6 +44,7 @@ func (s *IntegrationTestSuite) SetupTest() {
app := umeeapp.Setup(s.T())
ctx := app.NewContext(isCheckTx, tmproto.Header{
ChainID: fmt.Sprintf("test-chain-%s", tmrand.Str(4)),
Time: time.Now(),
})

oracle.InitGenesis(ctx, app.OracleKeeper, *types.DefaultGenesisState())
Expand Down Expand Up @@ -156,7 +158,9 @@ func (s *IntegrationTestSuite) TestEndBlockerVoteThreshold() {
for _, denom := range app.OracleKeeper.AcceptList(ctx) {
rate, err := app.OracleKeeper.GetExchangeRate(ctx, denom.SymbolDenom)
s.Require().NoError(err)
s.Require().Equal(types.ExchangeRate{Rate: sdk.OneDec(), Timestamp: ctx.BlockTime()},
s.Require().Equal(types.ExchangeRate{
Rate: sdk.OneDec(),
Timestamp: ctx.BlockTime()},
rate)
}

Expand Down Expand Up @@ -237,8 +241,8 @@ func (s *IntegrationTestSuite) TestEndBlockerVoteThreshold() {
rate, err := app.OracleKeeper.GetExchangeRate(ctx, "umee")
s.Require().NoError(err)
s.Require().Equal(types.ExchangeRate{Rate: sdk.OneDec(), Timestamp: ctx.BlockTime()}, rate)
rate, err = app.OracleKeeper.GetExchangeRate(ctx, "atom")
s.Require().ErrorIs(err, types.ErrUnknownDenom.Wrap("atom"))
rate, err = app.OracleKeeper.GetExchangeRate(ctx, "ATOM")
s.Require().ErrorIs(err, types.ErrUnknownDenom.Wrap("ATOM"))
s.Require().Equal(types.ExchangeRate{}, rate)
}

Expand Down
32 changes: 32 additions & 0 deletions x/oracle/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func GetQueryCmd() *cobra.Command {
QueryMissCounter(),
QuerySlashWindow(),
QueryHistoricAvgPrice(),
QueryExchangeRatesWithTimestamp(),
)

return cmd
Expand Down Expand Up @@ -300,3 +301,34 @@ func QueryHistoricAvgPrice() *cobra.Command {
flags.AddQueryFlagsToCmd(cmd)
return cmd
}

// QueryExchangeRatesWithTimestamp implements the query rate command.
func QueryExchangeRatesWithTimestamp() *cobra.Command {
cmd := &cobra.Command{
Use: "exg-rates-timestamp [denom]",
Args: cobra.MaximumNArgs(1),
Short: "Query the exchange rates with timestamp",
Long: strings.TrimSpace(`
Query the current exchange rates of assets based on USD with timestamp.
You can find the current list of active denoms by running
$ umeed query oracle exg-rates-timestamp
`),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
query := &types.QueryExgRatesWithTimestamp{}
if len(args) > 0 {
query.Denom = args[0]
}
res, err := queryClient.ExgRatesWithTimestamp(cmd.Context(), query)
return cli.PrintOrErr(res, err, clientCtx)
},
}

flags.AddQueryFlagsToCmd(cmd)
return cmd
}
53 changes: 53 additions & 0 deletions x/oracle/client/tests/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,56 @@ func (s *IntegrationTestSuite) TestQueryExchangeRate() {
})
}
}

func (s *IntegrationTestSuite) TestQueryExchangeRateWithTimestamp() {
val := s.network.Validators[0]
clientCtx := val.ClientCtx

testCases := []struct {
name string
args []string
expectErr bool
respType proto.Message
}{
{
name: "valid",
args: []string{
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
expectErr: false,
respType: &types.QueryExgRatesWithTimestampResponse{},
},
{
name: "valid denom",
args: []string{
"UMEE",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
expectErr: false,
respType: &types.QueryExgRatesWithTimestampResponse{},
},
{
name: "invalid denom",
args: []string{
"ABCD",
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
expectErr: true,
respType: &types.QueryExgRatesWithTimestampResponse{},
},
}

for _, tc := range testCases {
tc := tc

s.Run(tc.name, func() {
out, err := clitestutil.ExecTestCLICmd(clientCtx, cli.QueryExchangeRatesWithTimestamp(), tc.args)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
}
})
}
}
13 changes: 5 additions & 8 deletions x/oracle/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package oracle

import (
"fmt"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"

Expand All @@ -23,7 +24,7 @@ func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, genState types.GenesisSt
}

for _, ex := range genState.ExchangeRates {
keeper.SetExchangeRate(ctx, ex.Denom, ex.ExchangeRate)
keeper.SetExchangeRateWithTimestamp(ctx, ex.Denom, ex.Rate, ex.Timestamp)
}

for _, mc := range genState.MissCounters {
Expand Down Expand Up @@ -91,13 +92,9 @@ func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState {
return false
})

exchangeRates := []types.ExchangeRateTuple{}
keeper.IterateExchangeRates(ctx, func(denom string, rate sdk.Dec) (stop bool) {
exchangeRates = append(exchangeRates, types.ExchangeRateTuple{
Denom: denom,
ExchangeRate: rate,
})

exchangeRates := []types.DenomExchangeRate{}
keeper.IterateExchangeRates(ctx, func(denom string, er sdk.Dec, t time.Time) (stop bool) {
exchangeRates = append(exchangeRates, types.NewDenomExchangeRate(denom, er, t))
return false
})

Expand Down
20 changes: 12 additions & 8 deletions x/oracle/genesis_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package oracle_test

import (
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/umee-network/umee/v6/x/oracle"
"github.com/umee-network/umee/v6/x/oracle/types"
Expand Down Expand Up @@ -54,10 +56,11 @@ func (s *IntegrationTestSuite) TestGenesis_InitGenesis() {
"valid",
types.GenesisState{
Params: types.DefaultParams(),
ExchangeRates: types.ExchangeRateTuples{
types.ExchangeRateTuple{
Denom: denom,
ExchangeRate: exchangeRate,
ExchangeRates: []types.DenomExchangeRate{
{
Denom: denom,
Rate: exchangeRate,
Timestamp: time.Now(),
},
},
HistoricPrices: types.Prices{
Expand Down Expand Up @@ -152,10 +155,11 @@ func (s *IntegrationTestSuite) TestGenesis_ExportGenesis() {
ValidatorAddress: umeevaloperAddr,
},
}
exchangeRateTuples := types.ExchangeRateTuples{
types.ExchangeRateTuple{
Denom: upperDenom,
ExchangeRate: exchangeRate,
exchangeRateTuples := []types.DenomExchangeRate{
{
Denom: upperDenom,
Rate: exchangeRate,
Timestamp: ctx.BlockTime(),
},
}
missCounters := []types.MissCounter{
Expand Down
39 changes: 36 additions & 3 deletions x/oracle/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strings"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -64,8 +65,8 @@ func (q querier) ExchangeRates(

exchangeRates = exchangeRates.Add(sdk.NewDecCoinFromDec(req.Denom, exchangeRate.Rate))
} else {
q.IterateExchangeRates(ctx, func(denom string, rate sdk.Dec) (stop bool) {
exchangeRates = exchangeRates.Add(sdk.NewDecCoinFromDec(denom, rate))
q.IterateExchangeRates(ctx, func(denom string, exgRate sdk.Dec, _ time.Time) (stop bool) {
exchangeRates = exchangeRates.Add(sdk.NewDecCoinFromDec(denom, exgRate))
return false
})
}
Expand All @@ -85,7 +86,7 @@ func (q querier) ActiveExchangeRates(
ctx := sdk.UnwrapSDKContext(goCtx)

denoms := []string{}
q.IterateExchangeRates(ctx, func(denom string, _ sdk.Dec) (stop bool) {
q.IterateExchangeRates(ctx, func(denom string, _ sdk.Dec, _ time.Time) (stop bool) {
denoms = append(denoms, denom)
return false
})
Expand Down Expand Up @@ -329,3 +330,35 @@ func (q querier) AvgPrice(
}
return &types.QueryAvgPriceResponse{Price: p}, nil
}

// ExgRatesWithTimestamp queries exchange rates of all denoms with timestamp, or, if specified, returns
// a single denom.
func (q querier) ExgRatesWithTimestamp(
goCtx context.Context,
req *types.QueryExgRatesWithTimestamp,
) (*types.QueryExgRatesWithTimestampResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

ctx := sdk.UnwrapSDKContext(goCtx)

// TODO: need to decide if we want to return DecCoins here or list of ExchangeRates with denoms (we
// need the latter for genesis anyway)
var exgRates []types.DenomExchangeRate

if len(req.Denom) > 0 {
exchangeRate, err := q.GetExchangeRate(ctx, req.Denom)
if err != nil {
return nil, err
}
exgRates = append(exgRates, types.NewDenomExchangeRate(req.Denom, exchangeRate.Rate, exchangeRate.Timestamp))
} else {
q.IterateExchangeRates(ctx, func(denom string, exgRate sdk.Dec, t time.Time) (stop bool) {
exgRates = append(exgRates, types.NewDenomExchangeRate(denom, exgRate, t))
return false
})
}

return &types.QueryExgRatesWithTimestampResponse{ExgRates: exgRates}, nil
}
Loading

0 comments on commit 498bd30

Please sign in to comment.