Skip to content

Commit

Permalink
feat(oracle): include timestamp into save exchange rates (#2249)
Browse files Browse the repository at this point in the history
* feat(oracle): include timestamp into save exchange rates

* update implementations

* tests

* add comment

* linter

* leverage test

* Update tests and String

* keys_tests

* cosmetic

* ExchangeRate stringer

* cli test

* abci test
  • Loading branch information
robert-zaremba authored Sep 18, 2023
1 parent f865e4c commit 861208d
Show file tree
Hide file tree
Showing 17 changed files with 397 additions and 164 deletions.
1 change: 1 addition & 0 deletions proto/umee/oracle/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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
Expand Down
16 changes: 12 additions & 4 deletions proto/umee/oracle/v1/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ message AvgCounterParams {
// Denom - the object to hold configurations of each denom
message Denom {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (gogoproto.goproto_stringer) = false;

string base_denom = 1 [(gogoproto.moretags) = "yaml:\"base_denom\""];
Expand All @@ -94,7 +93,6 @@ message Denom {
// rate}{denom},...,{exchange rate}{denom}:{voter}")
message AggregateExchangeRatePrevote {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (gogoproto.goproto_stringer) = false;

string hash = 1 [(gogoproto.moretags) = "yaml:\"hash\""];
Expand All @@ -106,7 +104,6 @@ message AggregateExchangeRatePrevote {
// the exchange rates of USD denominated in various assets.
message AggregateExchangeRateVote {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (gogoproto.goproto_stringer) = false;

repeated ExchangeRateTuple exchange_rate_tuples = 1 [
Expand All @@ -121,7 +118,6 @@ message AggregateExchangeRateVote {
// ExchangeRateTuple - struct to store interpreted exchange rates data to store
message ExchangeRateTuple {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (gogoproto.goproto_stringer) = false;

string denom = 1 [(gogoproto.moretags) = "yaml:\"denom\""];
Expand All @@ -148,3 +144,15 @@ message AvgCounter {
// Unix timestamp when the first price was aggregated in the counter
google.protobuf.Timestamp start = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
}

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

string rate = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
google.protobuf.Timestamp timestamp = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
}
11 changes: 7 additions & 4 deletions x/leverage/keeper/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ func (k Keeper) TokenPrice(ctx sdk.Context, baseDenom string, mode types.PriceMo
mode = types.PriceModeSpot
}

var price, spotPrice, historicPrice sdk.Dec
var price, historicPrice sdk.Dec
var spotPrice oracletypes.ExchangeRate
if mode != types.PriceModeHistoric {
// spot price is required for modes other than historic
spotPrice, err = k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom)
Expand All @@ -56,15 +57,17 @@ func (k Keeper) TokenPrice(ctx sdk.Context, baseDenom string, mode types.PriceMo
}
}

// TODO: need to use spotPrice.Timestamp to make a decision about the price

switch mode {
case types.PriceModeSpot:
price = spotPrice
price = spotPrice.Rate
case types.PriceModeHistoric:
price = historicPrice
case types.PriceModeHigh:
price = sdk.MaxDec(spotPrice, historicPrice)
price = sdk.MaxDec(spotPrice.Rate, historicPrice)
case types.PriceModeLow:
price = sdk.MinDec(spotPrice, historicPrice)
price = sdk.MinDec(spotPrice.Rate, historicPrice)
default:
return sdk.ZeroDec(), t.Exponent, types.ErrInvalidPriceMode.Wrapf("%d", mode)
}
Expand Down
7 changes: 4 additions & 3 deletions x/leverage/keeper/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ func (m *mockOracleKeeper) MedianOfHistoricMedians(ctx sdk.Context, denom string
return p, uint32(numStamps), nil
}

func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (sdk.Dec, error) {
func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (oracletypes.ExchangeRate, error) {
p, ok := m.symbolExchangeRates[denom]
if !ok {
// This error matches oracle behavior on missing asset price
return sdk.ZeroDec(), oracletypes.ErrUnknownDenom.Wrap(denom)
return oracletypes.ExchangeRate{}, oracletypes.ErrUnknownDenom.Wrap(denom)
}

return p, nil
// TODO: add timestamp
return oracletypes.ExchangeRate{Rate: p}, nil
}

// Clear clears a denom from the mock oracle, simulating an outage.
Expand Down
4 changes: 3 additions & 1 deletion x/leverage/types/expected_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

oracle "github.com/umee-network/umee/v6/x/oracle/types"
)

// AccountKeeper defines the expected account keeper used for leverage simulations (noalias)
Expand Down Expand Up @@ -31,6 +33,6 @@ type BankKeeper interface {

// OracleKeeper defines the expected x/oracle keeper interface.
type OracleKeeper interface {
GetExchangeRate(ctx sdk.Context, denom string) (sdk.Dec, error)
GetExchangeRate(ctx sdk.Context, denom string) (oracle.ExchangeRate, error)
MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64) (sdk.Dec, uint32, error)
}
2 changes: 1 addition & 1 deletion x/oracle/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ func CalcPrices(ctx sdk.Context, params types.Params, k keeper.Keeper) error {
if err != nil {
return err
}

k.SetExchangeRateWithEvent(ctx, denom, exchangeRate)

if k.IsPeriodLastBlock(ctx, params.HistoricStampPeriod) {
k.AddHistoricPrice(ctx, denom, exchangeRate)
}
Expand Down
16 changes: 8 additions & 8 deletions x/oracle/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/staking/teststaking"
Expand Down Expand Up @@ -157,7 +156,8 @@ 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(sdk.MustNewDecFromStr("1.0"), rate)
s.Require().Equal(types.ExchangeRate{Rate: sdk.OneDec(), Timestamp: ctx.BlockTime()},
rate)
}

// Test: only val2 votes (has 39% vote power).
Expand All @@ -176,8 +176,8 @@ func (s *IntegrationTestSuite) TestEndBlockerVoteThreshold() {

for _, denom := range app.OracleKeeper.AcceptList(ctx) {
rate, err := app.OracleKeeper.GetExchangeRate(ctx, denom.SymbolDenom)
s.Require().ErrorIs(err, sdkerrors.Wrap(types.ErrUnknownDenom, denom.SymbolDenom))
s.Require().Equal(sdk.ZeroDec(), rate)
s.Require().ErrorIs(err, types.ErrUnknownDenom.Wrap(denom.SymbolDenom))
s.Require().Equal(types.ExchangeRate{}, rate)
}

// Test: val2 and val3 votes.
Expand All @@ -199,7 +199,7 @@ 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(sdk.MustNewDecFromStr("0.5"), rate)
s.Require().Equal(types.ExchangeRate{Rate: sdk.NewDecWithPrec(5, 1), Timestamp: ctx.BlockTime()}, rate)
}

// TODO: check reward distribution
Expand Down Expand Up @@ -236,10 +236,10 @@ func (s *IntegrationTestSuite) TestEndBlockerVoteThreshold() {

rate, err := app.OracleKeeper.GetExchangeRate(ctx, "umee")
s.Require().NoError(err)
s.Require().Equal(sdk.MustNewDecFromStr("1.0"), rate)
s.Require().Equal(types.ExchangeRate{Rate: sdk.OneDec(), Timestamp: ctx.BlockTime()}, rate)
rate, err = app.OracleKeeper.GetExchangeRate(ctx, "atom")
s.Require().ErrorIs(err, sdkerrors.Wrap(types.ErrUnknownDenom, "atom"))
s.Require().Equal(sdk.ZeroDec(), rate)
s.Require().ErrorIs(err, types.ErrUnknownDenom.Wrap("atom"))
s.Require().Equal(types.ExchangeRate{}, rate)
}

var exchangeRates = map[string][]sdk.Dec{
Expand Down
4 changes: 3 additions & 1 deletion x/oracle/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func (q querier) ExchangeRates(

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 exchangeRates sdk.DecCoins

if len(req.Denom) > 0 {
Expand All @@ -60,7 +62,7 @@ func (q querier) ExchangeRates(
return nil, err
}

exchangeRates = exchangeRates.Add(sdk.NewDecCoinFromDec(req.Denom, exchangeRate))
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))
Expand Down
40 changes: 19 additions & 21 deletions x/oracle/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package keeper

import (
"fmt"
"strings"

"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
Expand All @@ -12,7 +11,9 @@ import (
gogotypes "github.com/gogo/protobuf/types"
"github.com/tendermint/tendermint/libs/log"

"github.com/umee-network/umee/v6/util"
"github.com/umee-network/umee/v6/util/sdkutil"
"github.com/umee-network/umee/v6/util/store"
"github.com/umee-network/umee/v6/x/oracle/types"
)

Expand Down Expand Up @@ -72,22 +73,18 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger {

// GetExchangeRate gets the consensus exchange rate of USD denominated in the
// denom asset from the store.
func (k Keeper) GetExchangeRate(ctx sdk.Context, symbol string) (sdk.Dec, error) {
store := ctx.KVStore(k.storeKey)
symbol = strings.ToUpper(symbol)
b := store.Get(types.KeyExchangeRate(symbol))
if b == nil {
return sdk.ZeroDec(), types.ErrUnknownDenom.Wrap(symbol)
func (k Keeper) GetExchangeRate(ctx sdk.Context, symbol string) (types.ExchangeRate, error) {
v := store.GetValue[*types.ExchangeRate](ctx.KVStore(k.storeKey), types.KeyExchangeRate(symbol),
"exchange_rate")
if v == nil {
return types.ExchangeRate{}, types.ErrUnknownDenom.Wrap(symbol)
}

decProto := sdk.DecProto{}
k.cdc.MustUnmarshal(b, &decProto)

return decProto.Dec, nil
return *v, nil
}

// GetExchangeRateBase gets the consensus exchange rate of an asset
// in the base denom (e.g. ATOM -> uatom)
// TODO: needs to return timestamp as well
func (k Keeper) GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, error) {
var symbol string
var exponent uint64
Expand All @@ -110,16 +107,16 @@ func (k Keeper) GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, err
}

powerReduction := ten.Power(exponent)
return exchangeRate.Quo(powerReduction), nil
return exchangeRate.Rate.Quo(powerReduction), nil
}

// SetExchangeRate sets the consensus exchange rate of USD denominated in the
// denom asset to the store.
func (k Keeper) SetExchangeRate(ctx sdk.Context, denom string, exchangeRate sdk.Dec) {
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshal(&sdk.DecProto{Dec: exchangeRate})
denom = strings.ToUpper(denom)
store.Set(types.KeyExchangeRate(denom), bz)
func (k Keeper) SetExchangeRate(ctx sdk.Context, denom string, rate sdk.Dec) {
key := types.KeyExchangeRate(denom)
val := types.ExchangeRate{Rate: rate, Timestamp: ctx.BlockTime()}
err := store.SetValue(ctx.KVStore(k.storeKey), key, &val, "exchange_rate")
util.Panic(err)
}

// SetExchangeRateWithEvent sets an consensus
Expand All @@ -132,6 +129,7 @@ func (k Keeper) SetExchangeRateWithEvent(ctx sdk.Context, denom string, exchange
}

// IterateExchangeRates iterates over all USD rates in the store.
// TODO: handler should use ExchangeRate type rather than Dec
func (k Keeper) IterateExchangeRates(ctx sdk.Context, handler func(string, sdk.Dec) bool) {
store := ctx.KVStore(k.storeKey)
iter := sdk.KVStorePrefixIterator(store, types.KeyPrefixExchangeRate)
Expand All @@ -141,10 +139,10 @@ func (k Keeper) IterateExchangeRates(ctx sdk.Context, handler func(string, sdk.D
for ; iter.Valid(); iter.Next() {
key := iter.Key()
denom := string(key[prefixLen : len(key)-1]) // -1 to remove the null suffix
dp := sdk.DecProto{}
k.cdc.MustUnmarshal(iter.Value(), &dp)
var er types.ExchangeRate
k.cdc.MustUnmarshal(iter.Value(), &er)

if handler(denom, dp.Dec) {
if handler(denom, er.Rate) {
break
}
}
Expand Down
24 changes: 12 additions & 12 deletions x/oracle/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,11 @@ func (s *IntegrationTestSuite) TestAggregateExchangeRateVoteError() {
}

func (s *IntegrationTestSuite) TestSetExchangeRateWithEvent() {
app, ctx := s.app, s.ctx
app.OracleKeeper.SetExchangeRateWithEvent(ctx, displayDenom, sdk.OneDec())
rate, err := app.OracleKeeper.GetExchangeRate(ctx, displayDenom)
v := sdk.OneDec()
s.app.OracleKeeper.SetExchangeRateWithEvent(s.ctx, displayDenom, v)
rate, err := s.app.OracleKeeper.GetExchangeRate(s.ctx, displayDenom)
s.Require().NoError(err)
s.Require().Equal(rate, sdk.OneDec())
s.Require().Equal(rate, types.ExchangeRate{Rate: v, Timestamp: s.ctx.BlockTime()})
}

func (s *IntegrationTestSuite) TestGetExchangeRate_InvalidDenom() {
Expand All @@ -227,17 +227,17 @@ func (s *IntegrationTestSuite) TestGetExchangeRate_NotSet() {
}

func (s *IntegrationTestSuite) TestGetExchangeRate_Valid() {
app, ctx := s.app, s.ctx

app.OracleKeeper.SetExchangeRate(ctx, displayDenom, sdk.OneDec())
rate, err := app.OracleKeeper.GetExchangeRate(ctx, displayDenom)
v := sdk.OneDec()
expected := types.ExchangeRate{Rate: v, Timestamp: s.ctx.BlockTime()}
s.app.OracleKeeper.SetExchangeRate(s.ctx, displayDenom, v)
rate, err := s.app.OracleKeeper.GetExchangeRate(s.ctx, displayDenom)
s.Require().NoError(err)
s.Require().Equal(rate, sdk.OneDec())
s.Require().Equal(rate, expected)

app.OracleKeeper.SetExchangeRate(ctx, strings.ToLower(displayDenom), sdk.OneDec())
rate, err = app.OracleKeeper.GetExchangeRate(ctx, displayDenom)
s.app.OracleKeeper.SetExchangeRate(s.ctx, strings.ToLower(displayDenom), sdk.OneDec())
rate, err = s.app.OracleKeeper.GetExchangeRate(s.ctx, displayDenom)
s.Require().NoError(err)
s.Require().Equal(rate, sdk.OneDec())
s.Require().Equal(rate, expected)
}

func (s *IntegrationTestSuite) TestGetExchangeRateBase() {
Expand Down
5 changes: 3 additions & 2 deletions x/oracle/types/genesis.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion x/oracle/types/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package types

import (
"encoding/binary"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
Expand Down Expand Up @@ -33,7 +34,7 @@ var (
// KeyExchangeRate - stored by *denom*
func KeyExchangeRate(denom string) []byte {
// append 0 for null-termination
return util.ConcatBytes(1, KeyPrefixExchangeRate, []byte(denom))
return util.ConcatBytes(1, KeyPrefixExchangeRate, []byte(strings.ToUpper(denom)))
}

// KeyFeederDelegation - stored by *Validator* address
Expand Down
Loading

0 comments on commit 861208d

Please sign in to comment.