Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(WIP): adding timestamp to exchange rates of denoms #2243

Closed
wants to merge 8 commits into from
7 changes: 7 additions & 0 deletions app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,13 @@ func IntegrationTestNetworkConfig() network.Config {
oracleGenState.ExchangeRates = append(oracleGenState.ExchangeRates, oracletypes.NewExchangeRateTuple(
params.DisplayDenom, sdk.MustNewDecFromStr("34.21"),
))

oracleGenState.ExchangeRatesTimestamps = append(oracleGenState.ExchangeRatesTimestamps,
oracletypes.ExchangeRatesWithTimestamp{
ExchangeRateTuples: oracletypes.NewExchangeRateTuple(params.DisplayDenom, sdk.MustNewDecFromStr("34.21")),
Timestamp: 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
3 changes: 3 additions & 0 deletions proto/umee/oracle/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ message GenesisState {
(gogoproto.moretags) = "yaml:\"avg_counter_params\"",
(gogoproto.nullable) = false
];
repeated ExchangeRatesWithTimestamp exchange_rates_timestamps = 11 [
(gogoproto.nullable) = false
];
}

// FeederDelegation is the address for where oracle feeder authority are
Expand Down
11 changes: 11 additions & 0 deletions proto/umee/oracle/v1/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ message AggregateExchangeRateVote {
string voter = 2 [(gogoproto.moretags) = "yaml:\"voter\""];
}

// ExchangeRatesWithTimestamp - store the exchange rate of denom with price timestamp
message ExchangeRatesWithTimestamp {
ExchangeRateTuple exchange_rate_tuples = 1 [
Copy link
Member

Choose a reason for hiding this comment

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

is this repeated or single?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

single only

(gogoproto.moretags) = "yaml:\"exchange_rate_tuples\"",
(gogoproto.castrepeated) = "ExchangeRateTuples",
(gogoproto.nullable) = false
];
// Unix timestamp when the first price was aggregated in the counter
google.protobuf.Timestamp timestamp = 2 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
}

// ExchangeRateTuple - struct to store interpreted exchange rates data to store
message ExchangeRateTuple {
option (gogoproto.equal) = false;
Expand Down
20 changes: 20 additions & 0 deletions proto/umee/oracle/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ service Query {
option (google.api.http).get =
"/umee/historacle/v1/avg_price/{denom}";
}

// ExgRatesWithTimestamps returns exchange prices of denoms with timestamps.
rpc ExgRatesWithTimestamps(QueryExgRatesWithTimestamps)
returns (QueryExgRatesWithTimestampsResponse) {
option (google.api.http).get =
"/umee/historacle/v1/exg_rates";
}
}

// QueryExgRateWithTimestamps is the request type for the Query/ExgRatesWithTimestamps RPC
// method.
message QueryExgRatesWithTimestamps {
// denom defines the denomination to query for.
string denom = 1;
}

// QueryHistoricPricesResponse is response type for the
// Query/ExgRatesWithTimestamps RPC method.
message QueryExgRatesWithTimestampsResponse {
repeated ExchangeRatesWithTimestamp exg_rates = 1;
}

// QueryExchangeRates is the request type for the Query/ExchangeRate RPC
Expand Down
2 changes: 1 addition & 1 deletion x/leverage/simulation/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (s *SimTestSuite) SetupTest() {

// Use default umee token for sim tests
s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, fixtures.Token("uumee", "UMEE", 6)))
app.OracleKeeper.SetExchangeRate(ctx, "UMEE", sdk.MustNewDecFromStr("100.0"))
app.OracleKeeper.SetExchangeRateWithEvent(ctx, "UMEE", sdk.MustNewDecFromStr("100.0"))
for i := 1; i <= 24; i++ {
// set historic medians for UMEE on blocks 1-24 (without actually advancing block height)
// this is to accommodate leverage module's default 24 historic median requirement
Expand Down
8 changes: 5 additions & 3 deletions x/oracle/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ func (s *IntegrationTestSuite) TestEndBlockerVoteThreshold() {

// Test: only val2 votes (has 39% vote power).
// Total voting power per denom must be bigger or equal than 40% (see SetupTest).
// So if only val2 votes, we won't have any prices next block.
// So if only val2 votes, we won't have any prices next block but we retive price of denom with
// latest timestamp from store
// Note: GetExchangeRate now retrive the exchange rate from previous latest price
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + preVoteBlockDiff)
h = uint64(ctx.BlockHeight())
val2PreVotes.SubmitBlock = h
Expand All @@ -176,8 +178,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().NoError(err)
s.Require().Equal(sdk.MustNewDecFromStr("1.0"), rate)
}

// Test: val2 and val3 votes.
Expand Down
3 changes: 0 additions & 3 deletions x/oracle/client/tests/cli_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
//go:build norace
// +build norace

package tests

import (
Expand Down
8 changes: 8 additions & 0 deletions x/oracle/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, genState types.GenesisSt
keeper.SetFeederDelegation(ctx, voter, feeder)
}

for _, ex := range genState.ExchangeRatesTimestamps {
keeper.SetExchangeRateWithTimestamp(ctx, ex.ExchangeRateTuples.Denom,
ex.ExchangeRateTuples.ExchangeRate, ex.Timestamp,
)
}

for _, ex := range genState.ExchangeRates {
keeper.SetExchangeRate(ctx, ex.Denom, ex.ExchangeRate)
}
Expand Down Expand Up @@ -133,6 +139,7 @@ func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState {
medianPrices := keeper.AllMedianPrices(ctx)
medianDeviationPrices := keeper.AllMedianDeviationPrices(ctx)
hacp := keeper.GetHistoricAvgCounterParams(ctx)
exgRatesWithTimestamps := keeper.ExgRatesWithTimestamp(ctx)

return types.NewGenesisState(
params,
Expand All @@ -145,5 +152,6 @@ func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState {
medianPrices,
medianDeviationPrices,
hacp,
exgRatesWithTimestamps,
)
}
3 changes: 3 additions & 0 deletions x/oracle/keeper/end_blocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ func (k *Keeper) PruneAllPrices(ctx sdk.Context) {
}
}
}

// Deleting the old exchange rates of denoms and keep latest rates
k.PruneExgRates(ctx, params.HistoricStampPeriod)
}

// IsPeriodLastBlock returns true if we are at the last block of the period
Expand Down
29 changes: 29 additions & 0 deletions x/oracle/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"context"
"fmt"
"sort"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -327,3 +328,31 @@ func (q querier) AvgPrice(
}
return &types.QueryAvgPriceResponse{Price: p}, nil
}

// ExgRateWithTimestamps queries exchange rates of denoms with timestamp.
func (q querier) ExgRatesWithTimestamps(goCtx context.Context, req *types.QueryExgRatesWithTimestamps) (
*types.QueryExgRatesWithTimestampsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(goCtx)

exgRates := make([]*types.ExchangeRatesWithTimestamp, 0)
if len(req.Denom) != 0 {
q.IterateExgRatesWithTimestampForDenom(ctx, req.Denom, func(exgRate types.ExchangeRatesWithTimestamp) (stop bool) {
exgRates = append(exgRates, &exgRate)
return false
})
} else {
q.IterateExchangeRatesWithTimestamp(ctx, func(exgRate types.ExchangeRatesWithTimestamp) (stop bool) {
exgRates = append(exgRates, &exgRate)
return false
})
}

sort.Slice(exgRates, func(i, j int) bool {
return exgRates[i].Timestamp.After(exgRates[j].Timestamp)
})

return &types.QueryExgRatesWithTimestampsResponse{ExgRates: exgRates}, nil
}
2 changes: 1 addition & 1 deletion x/oracle/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (s *IntegrationTestSuite) TestQuerier_ActiveExchangeRates() {
}

func (s *IntegrationTestSuite) TestQuerier_ExchangeRates() {
s.app.OracleKeeper.SetExchangeRate(s.ctx, displayDenom, sdk.OneDec())
s.app.OracleKeeper.SetExchangeRateWithEvent(s.ctx, displayDenom, sdk.OneDec())
res, err := s.queryClient.ExchangeRates(s.ctx.Context(), &types.QueryExchangeRates{})
s.Require().NoError(err)
s.Require().Equal(sdk.DecCoins{
Expand Down
123 changes: 115 additions & 8 deletions x/oracle/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package keeper

import (
"fmt"
"sort"
"strings"
"time"

"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
Expand All @@ -12,6 +14,7 @@ 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/x/oracle/types"
)
Expand Down Expand Up @@ -73,17 +76,14 @@ 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 {

exgRates := k.ExgRatesWithTimestampForDenom(ctx, symbol)
if len(exgRates) == 0 {
return sdk.ZeroDec(), types.ErrUnknownDenom.Wrap(symbol)
}

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

return decProto.Dec, nil
// return latest exchange rate
return exgRates[0].ExchangeRateTuples.ExchangeRate, nil
}

// GetExchangeRateBase gets the consensus exchange rate of an asset
Expand Down Expand Up @@ -113,6 +113,112 @@ func (k Keeper) GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, err
return exchangeRate.Quo(powerReduction), nil
}

// SetExchangeRateWithTimestamp save the exchange of denom into store with key(denom,timestamp)
func (k Keeper) SetExchangeRateWithTimestamp(ctx sdk.Context, denom string, exchangeRate sdk.Dec, t time.Time) {
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshal(&types.ExchangeRatesWithTimestamp{
ExchangeRateTuples: types.ExchangeRateTuple{
Denom: denom,
ExchangeRate: exchangeRate,
},
Timestamp: t,
})
denom = strings.ToUpper(denom)
store.Set(types.KeyExchangeRateWithTimestamp(denom, t), bz)
}

// ExgRatesWithTimestamp returns all exchange rates with timestamps
func (k Keeper) ExgRatesWithTimestamp(ctx sdk.Context) []types.ExchangeRatesWithTimestamp {
exgRates := make([]types.ExchangeRatesWithTimestamp, 0)

k.IterateExchangeRatesWithTimestamp(ctx, func(exgRate types.ExchangeRatesWithTimestamp) (stop bool) {
exgRates = append(exgRates, exgRate)
return false
})

return exgRates
}

// ExgRatesWithTimestampForDenom returns exchange rates of given denom with timestamps.
func (k Keeper) ExgRatesWithTimestampForDenom(ctx sdk.Context, denom string) []types.ExchangeRatesWithTimestamp {
exgRates := make([]types.ExchangeRatesWithTimestamp, 0)

k.IterateExgRatesWithTimestampForDenom(ctx, denom, func(exgRate types.ExchangeRatesWithTimestamp) (stop bool) {
exgRates = append(exgRates, exgRate)
return false
})

sort.Slice(exgRates, func(i, j int) bool {
return exgRates[i].Timestamp.After(exgRates[j].Timestamp)
})

return exgRates
}

// IterateExchangeRates iterates over all USD rates in the store.
func (k Keeper) IterateExgRatesWithTimestampForDenom(ctx sdk.Context, denom string,
handler func(types.ExchangeRatesWithTimestamp) bool) {
store := ctx.KVStore(k.storeKey)
prefix := util.ConcatBytes(0, types.KeyPrefixExchangeRateWithTimeStamp, []byte(denom))
iter := sdk.KVStorePrefixIterator(store, prefix)
defer iter.Close()

for ; iter.Valid(); iter.Next() {
dp := types.ExchangeRatesWithTimestamp{}
k.cdc.MustUnmarshal(iter.Value(), &dp)

if handler(dp) {
break
}
}
}

// IterateExchangeRatesWithTimestamp iterates over all exchange rates in the store with denom and timestamp.
func (k Keeper) IterateExchangeRatesWithTimestamp(ctx sdk.Context,
handler func(types.ExchangeRatesWithTimestamp) bool) {
store := ctx.KVStore(k.storeKey)
iter := sdk.KVStorePrefixIterator(store, types.KeyPrefixExchangeRateWithTimeStamp)
defer iter.Close()

for ; iter.Valid(); iter.Next() {
dp := types.ExchangeRatesWithTimestamp{}
k.cdc.MustUnmarshal(iter.Value(), &dp)

if handler(dp) {
break
}
}
}

// PruneExgRates will delete exg rates of denoms and keep only latest timestamp noOfRecords.
func (k Keeper) PruneExgRates(ctx sdk.Context, noOfRecords uint64) {
exgRates := k.ExgRatesWithTimestamp(ctx)
exgRatesForDenom := make(map[string][]types.ExchangeRatesWithTimestamp, 0)
for _, er := range exgRates {
denom := er.ExchangeRateTuples.Denom
exgRatesForDenom[denom] = append(exgRatesForDenom[denom], er)
}

for _, v := range exgRatesForDenom {
if len(v) > int(noOfRecords) {
ers := v
// sort the list with descending order by timestamp
// only keep latest noOfRecords
sort.Slice(ers, func(i, j int) bool { return ers[i].Timestamp.After(ers[j].Timestamp) })
// exgRatesForDenom[k] = ers
for _, d := range ers[noOfRecords:] {
k.DeleteExgRateWithTimestamp(ctx, d.ExchangeRateTuples.Denom, d.Timestamp)
}
}
}
Comment on lines +202 to +213

Check warning

Code scanning / CodeQL

Iteration over map

Iteration over map may be a possible source of non-determinism
}

// DeleteExgRateWithTimestamp delete the record from store with denom exchange rate.
func (k Keeper) DeleteExgRateWithTimestamp(ctx sdk.Context, denom string, t time.Time) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.KeyExchangeRateWithTimestamp(denom, t))
}

// 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) {
Expand All @@ -126,6 +232,7 @@ func (k Keeper) SetExchangeRate(ctx sdk.Context, denom string, exchangeRate sdk.
// exchange rate to the store with ABCI event
func (k Keeper) SetExchangeRateWithEvent(ctx sdk.Context, denom string, exchangeRate sdk.Dec) {
k.SetExchangeRate(ctx, denom, exchangeRate)
k.SetExchangeRateWithTimestamp(ctx, denom, exchangeRate, ctx.BlockTime())
sdkutil.Emit(&ctx, &types.EventSetFxRate{
Denom: denom, Rate: exchangeRate,
})
Expand Down
8 changes: 4 additions & 4 deletions x/oracle/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,12 @@ func (s *IntegrationTestSuite) TestGetExchangeRate_NotSet() {
func (s *IntegrationTestSuite) TestGetExchangeRate_Valid() {
app, ctx := s.app, s.ctx

app.OracleKeeper.SetExchangeRate(ctx, displayDenom, sdk.OneDec())
app.OracleKeeper.SetExchangeRateWithEvent(ctx, displayDenom, sdk.OneDec())
rate, err := app.OracleKeeper.GetExchangeRate(ctx, displayDenom)
s.Require().NoError(err)
s.Require().Equal(rate, sdk.OneDec())

app.OracleKeeper.SetExchangeRate(ctx, strings.ToLower(displayDenom), sdk.OneDec())
app.OracleKeeper.SetExchangeRateWithEvent(ctx, strings.ToLower(displayDenom), sdk.OneDec())
rate, err = app.OracleKeeper.GetExchangeRate(ctx, displayDenom)
s.Require().NoError(err)
s.Require().Equal(rate, sdk.OneDec())
Expand All @@ -252,12 +252,12 @@ func (s *IntegrationTestSuite) TestGetExchangeRateBase() {

power := sdk.MustNewDecFromStr("10").Power(exponent)

s.app.OracleKeeper.SetExchangeRate(s.ctx, displayDenom, sdk.OneDec())
s.app.OracleKeeper.SetExchangeRateWithEvent(s.ctx, displayDenom, sdk.OneDec())
rate, err := s.app.OracleKeeper.GetExchangeRateBase(s.ctx, bondDenom)
s.Require().NoError(err)
s.Require().Equal(rate.Mul(power), sdk.OneDec())

s.app.OracleKeeper.SetExchangeRate(s.ctx, strings.ToLower(displayDenom), sdk.OneDec())
s.app.OracleKeeper.SetExchangeRateWithEvent(s.ctx, strings.ToLower(displayDenom), sdk.OneDec())
rate, err = s.app.OracleKeeper.GetExchangeRateBase(s.ctx, bondDenom)
s.Require().NoError(err)
s.Require().Equal(rate.Mul(power), sdk.OneDec())
Expand Down
Loading