-
Notifications
You must be signed in to change notification settings - Fork 193
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
- Loading branch information
1 parent
1588744
commit 585ebe7
Showing
7 changed files
with
266 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters