Skip to content

Commit 585ebe7

Browse files
k-yangUnique-Divineonikonychev
authored
feat(evm): add oracle precompile (#2056)
* feat(oracle): add solidity interface * feat(oracle): fix solidity IOracle.sol interface * feat(evm): add oracle precompile * chore: update changelog * Update x/evm/embeds/contracts/IOracle.sol Co-authored-by: Unique Divine <[email protected]> * Update x/evm/embeds/contracts/IOracle.sol Co-authored-by: Unique Divine <[email protected]> * Update x/evm/precompile/oracle.go Co-authored-by: Unique Divine <[email protected]> * Update x/evm/precompile/oracle.go Co-authored-by: Unique Divine <[email protected]> * Update IOracle.json * chore: linter --- Co-authored-by: Unique Divine <[email protected]> Co-authored-by: Unique Divine <[email protected]> Co-authored-by: Oleg Nikonychev <[email protected]>
1 parent 1588744 commit 585ebe7

File tree

7 files changed

+266
-3
lines changed

7 files changed

+266
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
109109
- [#2003](https://github.com/NibiruChain/nibiru/pull/2003) - fix(evm): fix FunToken conversions between Cosmos and EVM
110110
- [#2004](https://github.com/NibiruChain/nibiru/pull/2004) - refactor(evm)!: replace `HexAddr` with `EIP55Addr`
111111
- [#2006](https://github.com/NibiruChain/nibiru/pull/2006) - test(evm): e2e tests for eth_* endpoints
112-
- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups
112+
- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups
113113
- [#2013](https://github.com/NibiruChain/nibiru/pull/2013) - chore(evm): Set appropriate gas value for the required gas of the "IFunToken.sol" precompile.
114114
- [#2014](https://github.com/NibiruChain/nibiru/pull/2014) - feat(evm): Emit block bloom event in EndBlock hook.
115115
- [#2017](https://github.com/NibiruChain/nibiru/pull/2017) - fix(evm): Fix DynamicFeeTx gas cap parameters
@@ -119,13 +119,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119119
- [#2023](https://github.com/NibiruChain/nibiru/pull/2023) - fix(evm)!: adjusted generation and parsing of the block bloom events
120120
- [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces
121121
- [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options
122-
- [#2032](https://github.com/NibiruChain/nibiru/pull/2032) - feat(evm): ante handler to prohibit authz grant evm messages
122+
- [#2032](https://github.com/NibiruChain/nibiru/pull/2032) - feat(evm): ante handler to prohibit authz grant evm messages
123123
- [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code
124124
- [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented
125125
- [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs
126126
- [#2053](https://github.com/NibiruChain/nibiru/pull/2053) - refactor(evm): converted untyped event to typed and cleaned up
127127
- [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts.
128128
- [#2060](https://github.com/NibiruChain/nibiru/pull/2060) - fix(evm-precompiles): add assertNumArgs validation
129+
- [#2056](https://github.com/NibiruChain/nibiru/pull/2056) - feat(evm): add oracle precompile
129130

130131
#### Dapp modules: perp, spot, oracle, etc
131132

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"_format": "hh-sol-artifact-1",
3+
"contractName": "IOracle",
4+
"sourceName": "contracts/IOracle.sol",
5+
"abi": [
6+
{
7+
"inputs": [
8+
{
9+
"internalType": "string",
10+
"name": "pair",
11+
"type": "string"
12+
}
13+
],
14+
"name": "queryExchangeRate",
15+
"outputs": [
16+
{
17+
"internalType": "string",
18+
"name": "",
19+
"type": "string"
20+
}
21+
],
22+
"stateMutability": "view",
23+
"type": "function"
24+
}
25+
],
26+
"bytecode": "0x",
27+
"deployedBytecode": "0x",
28+
"linkReferences": {},
29+
"deployedLinkReferences": {}
30+
}

x/evm/embeds/contracts/IOracle.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.8.19;
3+
4+
/// @notice Oracle interface for querying exchange rates
5+
interface IOracle {
6+
/// @notice Queries the exchange rate for a given pair
7+
/// @param pair The asset pair to query. For example, "ubtc:uusd" is the
8+
/// USD price of BTC and "unibi:uusd" is the USD price of NIBI.
9+
/// @return The exchange rate (a decimal value) as a string.
10+
/// @dev This function is view-only and does not modify state.
11+
function queryExchangeRate(string memory pair) external view returns (string memory);
12+
}
13+
14+
address constant ORACLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801;
15+
16+
IOracle constant ORACLE_GATEWAY = IOracle(ORACLE_PRECOMPILE_ADDRESS);

x/evm/embeds/embeds.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
var (
1818
//go:embed artifacts/contracts/ERC20Minter.sol/ERC20Minter.json
1919
erc20MinterContractJSON []byte
20+
//go:embed artifacts/contracts/IOracle.sol/IOracle.json
21+
oracleContractJSON []byte
2022
//go:embed artifacts/contracts/FunToken.sol/IFunToken.json
2123
funtokenPrecompileJSON []byte
2224
//go:embed artifacts/contracts/Wasm.sol/IWasm.json
@@ -48,7 +50,10 @@ var (
4850
Name: "Wasm.sol",
4951
EmbedJSON: wasmPrecompileJSON,
5052
}
51-
53+
SmartContract_Oracle = CompiledEvmContract{
54+
Name: "Oracle.sol",
55+
EmbedJSON: oracleContractJSON,
56+
}
5257
SmartContract_TestERC20 = CompiledEvmContract{
5358
Name: "TestERC20.sol",
5459
EmbedJSON: testErc20Json,
@@ -59,6 +64,7 @@ func init() {
5964
SmartContract_ERC20Minter.MustLoad()
6065
SmartContract_FunToken.MustLoad()
6166
SmartContract_Wasm.MustLoad()
67+
SmartContract_Oracle.MustLoad()
6268
SmartContract_TestERC20.MustLoad()
6369
}
6470

x/evm/precompile/oracle.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package precompile
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
sdk "github.com/cosmos/cosmos-sdk/types"
8+
gethabi "github.com/ethereum/go-ethereum/accounts/abi"
9+
gethcommon "github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/vm"
11+
gethparams "github.com/ethereum/go-ethereum/params"
12+
13+
"github.com/NibiruChain/nibiru/v2/app/keepers"
14+
"github.com/NibiruChain/nibiru/v2/x/common/asset"
15+
"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
16+
"github.com/NibiruChain/nibiru/v2/x/evm/statedb"
17+
oraclekeeper "github.com/NibiruChain/nibiru/v2/x/oracle/keeper"
18+
)
19+
20+
var _ vm.PrecompiledContract = (*precompileOracle)(nil)
21+
22+
// Precompile address for "Oracle.sol", the contract that enables queries for exchange rates
23+
var PrecompileAddr_Oracle = gethcommon.HexToAddress("0x0000000000000000000000000000000000000801")
24+
25+
func (p precompileOracle) Address() gethcommon.Address {
26+
return PrecompileAddr_Oracle
27+
}
28+
29+
func (p precompileOracle) RequiredGas(input []byte) (gasPrice uint64) {
30+
// Since [gethparams.TxGas] is the cost per (Ethereum) transaction that does not create
31+
// a contract, it's value can be used to derive an appropriate value for the precompile call.
32+
return gethparams.TxGas
33+
}
34+
35+
const (
36+
OracleMethod_QueryExchangeRate OracleMethod = "queryExchangeRate"
37+
)
38+
39+
type OracleMethod string
40+
41+
// Run runs the precompiled contract
42+
func (p precompileOracle) Run(
43+
evm *vm.EVM, contract *vm.Contract, readonly bool,
44+
) (bz []byte, err error) {
45+
// This is a `defer` pattern to add behavior that runs in the case that the error is
46+
// non-nil, creating a concise way to add extra information.
47+
defer func() {
48+
if err != nil {
49+
precompileType := reflect.TypeOf(p).Name()
50+
err = fmt.Errorf("precompile error: failed to run %s: %w", precompileType, err)
51+
}
52+
}()
53+
54+
// 1 | Get context from StateDB
55+
stateDB, ok := evm.StateDB.(*statedb.StateDB)
56+
if !ok {
57+
err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB")
58+
return
59+
}
60+
ctx := stateDB.GetContext()
61+
62+
method, args, err := DecomposeInput(embeds.SmartContract_Oracle.ABI, contract.Input)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
switch OracleMethod(method.Name) {
68+
case OracleMethod_QueryExchangeRate:
69+
bz, err = p.queryExchangeRate(ctx, method, args, readonly)
70+
default:
71+
err = fmt.Errorf("invalid method called with name \"%s\"", method.Name)
72+
return
73+
}
74+
75+
return
76+
}
77+
78+
func PrecompileOracle(keepers keepers.PublicKeepers) vm.PrecompiledContract {
79+
return precompileOracle{
80+
oracleKeeper: keepers.OracleKeeper,
81+
}
82+
}
83+
84+
type precompileOracle struct {
85+
oracleKeeper oraclekeeper.Keeper
86+
}
87+
88+
func (p precompileOracle) queryExchangeRate(
89+
ctx sdk.Context,
90+
method *gethabi.Method,
91+
args []interface{},
92+
readOnly bool,
93+
) (bz []byte, err error) {
94+
pair, err := p.decomposeQueryExchangeRateArgs(args)
95+
if err != nil {
96+
return nil, err
97+
}
98+
assetPair, err := asset.TryNewPair(pair)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
price, err := p.oracleKeeper.GetExchangeRate(ctx, assetPair)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
return method.Outputs.Pack(price.String())
109+
}
110+
111+
func (p precompileOracle) decomposeQueryExchangeRateArgs(args []any) (
112+
pair string,
113+
err error,
114+
) {
115+
if len(args) != 1 {
116+
err = fmt.Errorf("expected 3 arguments but got %d", len(args))
117+
return
118+
}
119+
120+
pair, ok := args[0].(string)
121+
if !ok {
122+
err = ErrArgTypeValidation("string pair", args[0])
123+
return
124+
}
125+
126+
return pair, nil
127+
}

x/evm/precompile/oracle_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package precompile_test
2+
3+
import (
4+
"testing"
5+
6+
sdk "github.com/cosmos/cosmos-sdk/types"
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/stretchr/testify/suite"
9+
10+
"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
11+
"github.com/NibiruChain/nibiru/v2/x/evm/evmtest"
12+
"github.com/NibiruChain/nibiru/v2/x/evm/precompile"
13+
)
14+
15+
func (s *OracleSuite) TestOracle_FailToPackABI() {
16+
testcases := []struct {
17+
name string
18+
methodName string
19+
callArgs []any
20+
wantError string
21+
}{
22+
{
23+
name: "wrong amount of call args",
24+
methodName: string(precompile.OracleMethod_QueryExchangeRate),
25+
callArgs: []any{"nonsense", "args here", "to see if", "precompile is", "called"},
26+
wantError: "argument count mismatch: got 5 for 1",
27+
},
28+
{
29+
name: "wrong type for pair",
30+
methodName: string(precompile.OracleMethod_QueryExchangeRate),
31+
callArgs: []any{common.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6")},
32+
wantError: "abi: cannot use array as type string as argument",
33+
},
34+
{
35+
name: "invalid method name",
36+
methodName: "foo",
37+
callArgs: []any{"ubtc:uusdc"},
38+
wantError: "method 'foo' not found",
39+
},
40+
}
41+
42+
abi := embeds.SmartContract_Oracle.ABI
43+
44+
for _, tc := range testcases {
45+
s.Run(tc.name, func() {
46+
input, err := abi.Pack(tc.methodName, tc.callArgs...)
47+
s.ErrorContains(err, tc.wantError)
48+
s.Nil(input)
49+
})
50+
}
51+
}
52+
53+
func (s *OracleSuite) TestOracle_HappyPath() {
54+
deps := evmtest.NewTestDeps()
55+
56+
s.T().Log("Query exchange rate")
57+
{
58+
deps.App.OracleKeeper.SetPrice(deps.Ctx, "unibi:uusd", sdk.MustNewDecFromStr("0.067"))
59+
input, err := embeds.SmartContract_Oracle.ABI.Pack("queryExchangeRate", "unibi:uusd")
60+
s.NoError(err)
61+
resp, err := deps.EvmKeeper.CallContractWithInput(
62+
deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Oracle, true, input,
63+
)
64+
s.NoError(err)
65+
66+
// Check the response
67+
out, err := embeds.SmartContract_Oracle.ABI.Unpack(string(precompile.OracleMethod_QueryExchangeRate), resp.Ret)
68+
s.NoError(err)
69+
70+
// Check the response
71+
s.Equal("0.067000000000000000", out[0].(string))
72+
}
73+
}
74+
75+
type OracleSuite struct {
76+
suite.Suite
77+
}
78+
79+
// TestPrecompileSuite: Runs all the tests in the suite.
80+
func TestOracleSuite(t *testing.T) {
81+
suite.Run(t, new(OracleSuite))
82+
}

x/evm/precompile/precompile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func InitPrecompiles(
4949
for _, precompileSetupFn := range []func(k keepers.PublicKeepers) vm.PrecompiledContract{
5050
PrecompileFunToken,
5151
PrecompileWasm,
52+
PrecompileOracle,
5253
} {
5354
pc := precompileSetupFn(k)
5455
precompiles[pc.Address()] = pc

0 commit comments

Comments
 (0)