Skip to content

Commit

Permalink
feat(evm): add oracle precompile (#2056)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Oct 3, 2024
1 parent 1588744 commit 585ebe7
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 3 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#2003](https://github.com/NibiruChain/nibiru/pull/2003) - fix(evm): fix FunToken conversions between Cosmos and EVM
- [#2004](https://github.com/NibiruChain/nibiru/pull/2004) - refactor(evm)!: replace `HexAddr` with `EIP55Addr`
- [#2006](https://github.com/NibiruChain/nibiru/pull/2006) - test(evm): e2e tests for eth_* endpoints
- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups
- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups
- [#2013](https://github.com/NibiruChain/nibiru/pull/2013) - chore(evm): Set appropriate gas value for the required gas of the "IFunToken.sol" precompile.
- [#2014](https://github.com/NibiruChain/nibiru/pull/2014) - feat(evm): Emit block bloom event in EndBlock hook.
- [#2017](https://github.com/NibiruChain/nibiru/pull/2017) - fix(evm): Fix DynamicFeeTx gas cap parameters
Expand All @@ -119,13 +119,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#2023](https://github.com/NibiruChain/nibiru/pull/2023) - fix(evm)!: adjusted generation and parsing of the block bloom events
- [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces
- [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options
- [#2032](https://github.com/NibiruChain/nibiru/pull/2032) - feat(evm): ante handler to prohibit authz grant evm messages
- [#2032](https://github.com/NibiruChain/nibiru/pull/2032) - feat(evm): ante handler to prohibit authz grant evm messages
- [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code
- [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented
- [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs
- [#2053](https://github.com/NibiruChain/nibiru/pull/2053) - refactor(evm): converted untyped event to typed and cleaned up
- [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts.
- [#2060](https://github.com/NibiruChain/nibiru/pull/2060) - fix(evm-precompiles): add assertNumArgs validation
- [#2056](https://github.com/NibiruChain/nibiru/pull/2056) - feat(evm): add oracle precompile

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

Expand Down
30 changes: 30 additions & 0 deletions x/evm/embeds/artifacts/contracts/IOracle.sol/IOracle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "IOracle",
"sourceName": "contracts/IOracle.sol",
"abi": [
{
"inputs": [
{
"internalType": "string",
"name": "pair",
"type": "string"
}
],
"name": "queryExchangeRate",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}
16 changes: 16 additions & 0 deletions x/evm/embeds/contracts/IOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19;

/// @notice Oracle interface for querying exchange rates
interface IOracle {
/// @notice Queries the exchange rate for a given pair
/// @param pair The asset pair to query. For example, "ubtc:uusd" is the
/// USD price of BTC and "unibi:uusd" is the USD price of NIBI.
/// @return The exchange rate (a decimal value) as a string.
/// @dev This function is view-only and does not modify state.
function queryExchangeRate(string memory pair) external view returns (string memory);
}

address constant ORACLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801;

IOracle constant ORACLE_GATEWAY = IOracle(ORACLE_PRECOMPILE_ADDRESS);
8 changes: 7 additions & 1 deletion x/evm/embeds/embeds.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
var (
//go:embed artifacts/contracts/ERC20Minter.sol/ERC20Minter.json
erc20MinterContractJSON []byte
//go:embed artifacts/contracts/IOracle.sol/IOracle.json
oracleContractJSON []byte
//go:embed artifacts/contracts/FunToken.sol/IFunToken.json
funtokenPrecompileJSON []byte
//go:embed artifacts/contracts/Wasm.sol/IWasm.json
Expand Down Expand Up @@ -48,7 +50,10 @@ var (
Name: "Wasm.sol",
EmbedJSON: wasmPrecompileJSON,
}

SmartContract_Oracle = CompiledEvmContract{
Name: "Oracle.sol",
EmbedJSON: oracleContractJSON,
}
SmartContract_TestERC20 = CompiledEvmContract{
Name: "TestERC20.sol",
EmbedJSON: testErc20Json,
Expand All @@ -59,6 +64,7 @@ func init() {
SmartContract_ERC20Minter.MustLoad()
SmartContract_FunToken.MustLoad()
SmartContract_Wasm.MustLoad()
SmartContract_Oracle.MustLoad()
SmartContract_TestERC20.MustLoad()
}

Expand Down
127 changes: 127 additions & 0 deletions x/evm/precompile/oracle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package precompile

import (
"fmt"
"reflect"

sdk "github.com/cosmos/cosmos-sdk/types"
gethabi "github.com/ethereum/go-ethereum/accounts/abi"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
gethparams "github.com/ethereum/go-ethereum/params"

"github.com/NibiruChain/nibiru/v2/app/keepers"
"github.com/NibiruChain/nibiru/v2/x/common/asset"
"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
"github.com/NibiruChain/nibiru/v2/x/evm/statedb"
oraclekeeper "github.com/NibiruChain/nibiru/v2/x/oracle/keeper"
)

var _ vm.PrecompiledContract = (*precompileOracle)(nil)

// Precompile address for "Oracle.sol", the contract that enables queries for exchange rates
var PrecompileAddr_Oracle = gethcommon.HexToAddress("0x0000000000000000000000000000000000000801")

func (p precompileOracle) Address() gethcommon.Address {
return PrecompileAddr_Oracle
}

func (p precompileOracle) RequiredGas(input []byte) (gasPrice uint64) {
// Since [gethparams.TxGas] is the cost per (Ethereum) transaction that does not create
// a contract, it's value can be used to derive an appropriate value for the precompile call.
return gethparams.TxGas
}

const (
OracleMethod_QueryExchangeRate OracleMethod = "queryExchangeRate"
)

type OracleMethod string

// Run runs the precompiled contract
func (p precompileOracle) Run(
evm *vm.EVM, contract *vm.Contract, readonly bool,
) (bz []byte, err error) {
// This is a `defer` pattern to add behavior that runs in the case that the error is
// non-nil, creating a concise way to add extra information.
defer func() {
if err != nil {
precompileType := reflect.TypeOf(p).Name()
err = fmt.Errorf("precompile error: failed to run %s: %w", precompileType, err)
}
}()

// 1 | Get context from StateDB
stateDB, ok := evm.StateDB.(*statedb.StateDB)
if !ok {
err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB")
return
}
ctx := stateDB.GetContext()

method, args, err := DecomposeInput(embeds.SmartContract_Oracle.ABI, contract.Input)
if err != nil {
return nil, err
}

switch OracleMethod(method.Name) {
case OracleMethod_QueryExchangeRate:
bz, err = p.queryExchangeRate(ctx, method, args, readonly)
default:
err = fmt.Errorf("invalid method called with name \"%s\"", method.Name)
return
}

return
}

func PrecompileOracle(keepers keepers.PublicKeepers) vm.PrecompiledContract {
return precompileOracle{
oracleKeeper: keepers.OracleKeeper,
}
}

type precompileOracle struct {
oracleKeeper oraclekeeper.Keeper
}

func (p precompileOracle) queryExchangeRate(
ctx sdk.Context,
method *gethabi.Method,
args []interface{},
readOnly bool,
) (bz []byte, err error) {
pair, err := p.decomposeQueryExchangeRateArgs(args)
if err != nil {
return nil, err
}
assetPair, err := asset.TryNewPair(pair)
if err != nil {
return nil, err
}

price, err := p.oracleKeeper.GetExchangeRate(ctx, assetPair)
if err != nil {
return nil, err
}

return method.Outputs.Pack(price.String())
}

func (p precompileOracle) decomposeQueryExchangeRateArgs(args []any) (
pair string,
err error,
) {
if len(args) != 1 {
err = fmt.Errorf("expected 3 arguments but got %d", len(args))
return
}

pair, ok := args[0].(string)
if !ok {
err = ErrArgTypeValidation("string pair", args[0])
return
}

return pair, nil
}
82 changes: 82 additions & 0 deletions x/evm/precompile/oracle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package precompile_test

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/suite"

"github.com/NibiruChain/nibiru/v2/x/evm/embeds"
"github.com/NibiruChain/nibiru/v2/x/evm/evmtest"
"github.com/NibiruChain/nibiru/v2/x/evm/precompile"
)

func (s *OracleSuite) TestOracle_FailToPackABI() {
testcases := []struct {
name string
methodName string
callArgs []any
wantError string
}{
{
name: "wrong amount of call args",
methodName: string(precompile.OracleMethod_QueryExchangeRate),
callArgs: []any{"nonsense", "args here", "to see if", "precompile is", "called"},
wantError: "argument count mismatch: got 5 for 1",
},
{
name: "wrong type for pair",
methodName: string(precompile.OracleMethod_QueryExchangeRate),
callArgs: []any{common.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6")},
wantError: "abi: cannot use array as type string as argument",
},
{
name: "invalid method name",
methodName: "foo",
callArgs: []any{"ubtc:uusdc"},
wantError: "method 'foo' not found",
},
}

abi := embeds.SmartContract_Oracle.ABI

for _, tc := range testcases {
s.Run(tc.name, func() {
input, err := abi.Pack(tc.methodName, tc.callArgs...)
s.ErrorContains(err, tc.wantError)
s.Nil(input)
})
}
}

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

s.T().Log("Query exchange rate")
{
deps.App.OracleKeeper.SetPrice(deps.Ctx, "unibi:uusd", sdk.MustNewDecFromStr("0.067"))
input, err := embeds.SmartContract_Oracle.ABI.Pack("queryExchangeRate", "unibi:uusd")
s.NoError(err)
resp, err := deps.EvmKeeper.CallContractWithInput(
deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Oracle, true, input,
)
s.NoError(err)

// Check the response
out, err := embeds.SmartContract_Oracle.ABI.Unpack(string(precompile.OracleMethod_QueryExchangeRate), resp.Ret)
s.NoError(err)

// Check the response
s.Equal("0.067000000000000000", out[0].(string))
}
}

type OracleSuite struct {
suite.Suite
}

// TestPrecompileSuite: Runs all the tests in the suite.
func TestOracleSuite(t *testing.T) {
suite.Run(t, new(OracleSuite))
}
1 change: 1 addition & 0 deletions x/evm/precompile/precompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func InitPrecompiles(
for _, precompileSetupFn := range []func(k keepers.PublicKeepers) vm.PrecompiledContract{
PrecompileFunToken,
PrecompileWasm,
PrecompileOracle,
} {
pc := precompileSetupFn(k)
precompiles[pc.Address()] = pc
Expand Down

0 comments on commit 585ebe7

Please sign in to comment.