Skip to content

Commit

Permalink
fix(oracle): The timestamps resulting from ctx.WithBlock* don't actually
Browse files Browse the repository at this point in the history
correspond to the block header information from specified blocks in the
chain's history, so the oracle exchange rates need a way to correctly
retrieve this information. This change fixes that discrepency, giving
the expected block timesamp for the EVM's oracle precompiled contract.
The change also simplifies and corrects the code in x/oracle.

refactor(oralce): rename querier -> grpc_query for consistency with the rest of the codebase
  • Loading branch information
Unique-Divine committed Nov 26, 2024
1 parent 7d91c5a commit 80e4637
Show file tree
Hide file tree
Showing 15 changed files with 487 additions and 675 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ tests for race conditions within funtoken precompile
- [#2111](https://github.com/NibiruChain/nibiru/pull/2111) - fix: e2e-evm-cron.yml
- [#2114](https://github.com/NibiruChain/nibiru/pull/2114) - fix(evm): make gas cost zero in conditional bank keeper flow
- [#2116](https://github.com/NibiruChain/nibiru/pull/2116) - fix(precompile-funtoken.go): Fixes a bug where the err != nil check is missing in the bankBalance precompile method
- [#2117](https://github.com/NibiruChain/nibiru/pull/2117) - fix(oracle): The
timestamps resulting from ctx.WithBlock* don't actually correspond to the block
header information from specified blocks in the chain's history, so the oracle
exchange rates need a way to correctly retrieve this information. This change
fixes that discrepency, giving the expected block timesamp for the EVM's oracle
precompiled contract. The change also simplifies and corrects the code in x/oracle.

#### Nibiru EVM | Before Audit 1 - 2024-10-18

Expand Down
1 change: 0 additions & 1 deletion app/wasmext/stargate_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ func WasmAcceptedStargateQueries() wasmkeeper.AcceptedStargateQueries {

// nibiru oracle
"/nibiru.oracle.v1.Query/ExchangeRate": new(oracle.QueryExchangeRateResponse),
"/nibiru.oracle.v1.Query/DatedExchangeRate": new(oracle.QueryDatedExchangeRateResponse),
"/nibiru.oracle.v1.Query/ExchangeRateTwap": new(oracle.QueryExchangeRateResponse),
"/nibiru.oracle.v1.Query/ExchangeRates": new(oracle.QueryExchangeRatesResponse),
"/nibiru.oracle.v1.Query/Actives": new(oracle.QueryActivesResponse),
Expand Down
4 changes: 3 additions & 1 deletion eth/rpc/backend/blocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ func (s *BackendSuite) TestBlockNumber() {
latestHeight, _ := s.network.LatestHeight()
resp, err := s.backend.BlockNumber()
s.Require().NoError(err, resp)
s.Require().Equal(uint64(latestHeight), uint64(blockHeight))
// Rather than checking exact equality, which might not be true due to
// latency. Add a cushion of 2 blocks.
s.Require().LessOrEqual(uint64(latestHeight)-uint64(blockHeight), 2)
}

func (s *BackendSuite) TestGetBlockByNumberr() {
Expand Down
7 changes: 6 additions & 1 deletion proto/nibiru/oracle/v1/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,19 @@ message ExchangeRateTuple {
];
}

message DatedPrice {
message ExchangeRateAtBlock {
string exchange_rate = 1 [
(gogoproto.moretags) = "yaml:\"exchange_rate\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

uint64 created_block = 2 [ (gogoproto.moretags) = "yaml:\"created_block\"" ];

// Block timestamp for the block where the oracle came to consensus for this
// price. This timestamp is a conventional Unix millisecond time, i.e. the
// number of milliseconds elapsed since January 1, 1970 UTC.
int64 block_timestamp_ms = 3 [ (gogoproto.moretags) = "yaml:\"block_timestamp_ms\"" ];
}

// Rewards defines a credit object towards validators
Expand Down
28 changes: 7 additions & 21 deletions proto/nibiru/oracle/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ option go_package = "github.com/NibiruChain/nibiru/v2/x/oracle/types";

// Query defines the gRPC querier service.
service Query {

Check failure on line 12 in proto/nibiru/oracle/v1/query.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present RPC "DatedExchangeRate" on service "Query" was deleted.
// ExchangeRate returns exchange rate of a pair
// ExchangeRate returns exchange rate of a pair along with the block height and
// block time that the exchange rate was set by the oracle module.
rpc ExchangeRate(QueryExchangeRateRequest)
returns (QueryExchangeRateResponse) {
option (google.api.http).get = "/nibiru/oracle/v1beta1/exchange_rate";
Expand All @@ -22,12 +23,6 @@ service Query {
option (google.api.http).get = "/nibiru/oracle/v1beta1/exchange_rate_twap";
}

// DatedExchangeRate returns latest price of a pair
rpc DatedExchangeRate(QueryExchangeRateRequest)
returns (QueryDatedExchangeRateResponse) {
option (google.api.http).get = "/nibiru/oracle/v1beta1/dated_exchange_rate";
}

// ExchangeRates returns exchange rates of all pairs
rpc ExchangeRates(QueryExchangeRatesRequest)
returns (QueryExchangeRatesResponse) {
Expand Down Expand Up @@ -114,20 +109,6 @@ message QueryExchangeRateResponse {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

// QueryExchangeRatesRequest is the request type for the Query/ExchangeRates RPC
// method.
message QueryExchangeRatesRequest {
}

// QueryDatedExchangeRateResponse is the request type for the
// Query/DatedExchangeRate RPC method.
message QueryDatedExchangeRateResponse {
string price = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Block timestamp for the block where the oracle came to consensus for this
// price. This timestamp is a conventional Unix millisecond time, i.e. the
Expand All @@ -138,6 +119,11 @@ message QueryDatedExchangeRateResponse {
uint64 block_height = 3;
}

// QueryExchangeRatesRequest is the request type for the Query/ExchangeRates RPC
// method.
message QueryExchangeRatesRequest {
}

// QueryExchangeRatesResponse is response type for the
// Query/ExchangeRates RPC method.
message QueryExchangeRatesResponse {
Expand Down
8 changes: 6 additions & 2 deletions x/evm/precompile/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,16 @@ func (p precompileOracle) queryExchangeRate(
return nil, err
}

price, blockTime, blockHeight, err := p.oracleKeeper.GetDatedExchangeRate(ctx, assetPair)
priceAtBlock, err := p.oracleKeeper.ExchangeRates.Get(ctx, assetPair)
if err != nil {
return nil, err
}

return method.Outputs.Pack(price.BigInt(), uint64(blockTime), blockHeight)
return method.Outputs.Pack(
priceAtBlock.ExchangeRate.BigInt(),
uint64(priceAtBlock.BlockTimestampMs),
priceAtBlock.CreatedBlock,
)
}

func (p precompileOracle) parseQueryExchangeRateArgs(args []any) (
Expand Down
55 changes: 43 additions & 12 deletions x/evm/precompile/oracle_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package precompile_test

import (
"fmt"
"math/big"
"testing"
"time"
Expand All @@ -9,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/suite"

"github.com/NibiruChain/nibiru/v2/x/evm"
"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
"github.com/NibiruChain/nibiru/v2/x/evm/evmtest"
"github.com/NibiruChain/nibiru/v2/x/evm/precompile"
Expand Down Expand Up @@ -56,14 +58,12 @@ func (s *OracleSuite) TestOracle_FailToPackABI() {

func (s *OracleSuite) TestOracle_HappyPath() {
deps := evmtest.NewTestDeps()

s.T().Log("Query exchange rate")
{
deps.Ctx = deps.Ctx.WithBlockTime(time.Unix(69, 420)).WithBlockHeight(69)
deps.App.OracleKeeper.SetPrice(deps.Ctx, "unibi:uusd", sdk.MustNewDecFromStr("0.067"))

resp, err := deps.EvmKeeper.CallContract(
deps.Ctx,
runQuery := func(ctx sdk.Context) (
resp *evm.MsgEthereumTxResponse,
err error,
) {
return deps.EvmKeeper.CallContract(
ctx,
embeds.SmartContract_Oracle.ABI,
deps.Sender.EthAddr,
&precompile.PrecompileAddr_Oracle,
Expand All @@ -72,16 +72,47 @@ func (s *OracleSuite) TestOracle_HappyPath() {
"queryExchangeRate",
"unibi:uusd",
)
}

s.T().Log("Query exchange rate")
{
// 69 seconds + 420 nanoseconds === 69000 milliseconds for the
// return value from the UnixMilli() function
deps.Ctx = deps.Ctx.WithBlockTime(time.Unix(69, 420)).WithBlockHeight(69)
deps.App.OracleKeeper.SetPrice(deps.Ctx, "unibi:uusd", sdk.MustNewDecFromStr("0.067"))

resp, err := runQuery(deps.Ctx)
s.NoError(err)

// Check the response
out, err := embeds.SmartContract_Oracle.ABI.Unpack(string(precompile.OracleMethod_queryExchangeRate), resp.Ret)
out, err := embeds.SmartContract_Oracle.ABI.Unpack(
string(precompile.OracleMethod_queryExchangeRate), resp.Ret,
)
s.NoError(err)
s.Equal(out[0].(*big.Int), big.NewInt(67_000_000_000_000_000))
s.Equal(fmt.Sprintf("%d", out[1].(uint64)), "69000")
s.Equal(fmt.Sprintf("%d", out[2].(uint64)), "69")
}

s.T().Log("Query from a later time")
{
secondsLater := deps.Ctx.BlockTime().Add(100 * time.Second)
resp, err := runQuery(deps.Ctx.
WithBlockTime(secondsLater).
WithBlockHeight(deps.Ctx.BlockHeight() + 50),
)
s.NoError(err)

// Check the response
s.Equal(out[0].(*big.Int), big.NewInt(67000000000000000))
s.Equal(out[1].(uint64), uint64(69000))
s.Equal(out[2].(uint64), uint64(69))
out, err := embeds.SmartContract_Oracle.ABI.Unpack(
string(precompile.OracleMethod_queryExchangeRate), resp.Ret,
)
s.NoError(err)
// These terms should still be equal because the latest exchange rate
// has not changed.
s.Equal(out[0].(*big.Int), big.NewInt(67_000_000_000_000_000))
s.Equal(fmt.Sprintf("%d", out[1].(uint64)), "69000")
s.Equal(fmt.Sprintf("%d", out[2].(uint64)), "69")
}
}

Expand Down
7 changes: 6 additions & 1 deletion x/oracle/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ func TestExportInitGenesis(t *testing.T) {

input.OracleKeeper.Params.Set(input.Ctx, types.DefaultParams())
input.OracleKeeper.FeederDelegations.Insert(input.Ctx, keeper.ValAddrs[0], keeper.Addrs[1])
input.OracleKeeper.ExchangeRates.Insert(input.Ctx, "pair1:pair2", types.DatedPrice{ExchangeRate: math.LegacyNewDec(123), CreatedBlock: 0})
input.OracleKeeper.ExchangeRates.Insert(input.Ctx, "pair1:pair2",
types.ExchangeRateAtBlock{
ExchangeRate: math.LegacyNewDec(123),
CreatedBlock: 0,
BlockTimestampMs: 0,
})
input.OracleKeeper.Prevotes.Insert(input.Ctx, keeper.ValAddrs[0], types.NewAggregateExchangeRatePrevote(types.AggregateVoteHash{123}, keeper.ValAddrs[0], uint64(2)))
input.OracleKeeper.Votes.Insert(input.Ctx, keeper.ValAddrs[0], types.NewAggregateExchangeRateVote(types.ExchangeRateTuples{{Pair: "foo", ExchangeRate: math.LegacyNewDec(123)}}, keeper.ValAddrs[0]))
input.OracleKeeper.WhitelistedPairs.Insert(input.Ctx, "pair1:pair1")
Expand Down
27 changes: 6 additions & 21 deletions x/oracle/keeper/querier.go → x/oracle/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,15 @@ func (q querier) ExchangeRate(c context.Context, req *types.QueryExchangeRateReq
}

ctx := sdk.UnwrapSDKContext(c)
exchangeRate, err := q.Keeper.GetExchangeRate(ctx, req.Pair)
out, err := q.Keeper.ExchangeRates.Get(ctx, req.Pair)
if err != nil {
return nil, err
}

return &types.QueryExchangeRateResponse{ExchangeRate: exchangeRate}, nil
return &types.QueryExchangeRateResponse{
ExchangeRate: out.ExchangeRate,
BlockTimestampMs: out.BlockTimestampMs,
BlockHeight: out.CreatedBlock,
}, nil
}

/*
Expand All @@ -79,24 +82,6 @@ func (q querier) ExchangeRateTwap(c context.Context, req *types.QueryExchangeRat
return &types.QueryExchangeRateResponse{ExchangeRate: twap}, nil
}

// get the latest price snapshot from the oracle for a pair
func (q querier) DatedExchangeRate(c context.Context, req *types.QueryExchangeRateRequest) (response *types.QueryDatedExchangeRateResponse, err error) {
if _, err = q.ExchangeRate(c, req); err != nil {
return
}

ctx := sdk.UnwrapSDKContext(c)
price, blockTime, blockHeight, err := q.Keeper.GetDatedExchangeRate(ctx, req.Pair)
if err != nil {
return &types.QueryDatedExchangeRateResponse{}, err
}
return &types.QueryDatedExchangeRateResponse{
Price: price,
BlockTimestampMs: blockTime,
BlockHeight: blockHeight,
}, nil
}

// ExchangeRates queries exchange rates of all pairs
func (q querier) ExchangeRates(c context.Context, _ *types.QueryExchangeRatesRequest) (*types.QueryExchangeRatesResponse, error) {
ctx := sdk.UnwrapSDKContext(c)
Expand Down
Loading

0 comments on commit 80e4637

Please sign in to comment.