Skip to content

Oracle Precompile #1445

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

Merged
merged 16 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ func New(
stakingkeeper.NewMsgServerImpl(app.StakingKeeper),
app.GovKeeper,
app.DistrKeeper,
app.OracleKeeper,
); err != nil {
panic(err)
}
Expand Down
44 changes: 44 additions & 0 deletions contracts/test/EVMPrecompileTester.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,50 @@ describe("EVM Test", function () {
// TODO: Add staking query precompile here
});
});

describe("EVM Oracle Precompile Tester", function () {
const OraclePrecompileContract = '0x0000000000000000000000000000000000001008';
before(async function() {
const exchangeRatesContent = readDeploymentOutput('oracle_exchange_rates.json');
const twapsContent = readDeploymentOutput('oracle_twaps.json');

exchangeRatesJSON = JSON.parse(exchangeRatesContent).denom_oracle_exchange_rate_pairs;
twapsJSON = JSON.parse(twapsContent).oracle_twaps;

const [signer, _] = await ethers.getSigners();
owner = await signer.getAddress();

const contractABIPath = path.join(__dirname, '../../precompiles/oracle/abi.json');
const contractABI = require(contractABIPath);
// Get a contract instance
oracle = new ethers.Contract(OraclePrecompileContract, contractABI, signer);
});

it("Oracle Exchange Rates", async function () {
const exchangeRates = await oracle.getExchangeRates();
const exchangeRatesLen = exchangeRatesJSON.length;
expect(exchangeRates.length).to.equal(exchangeRatesLen);

for (let i = 0; i < exchangeRatesLen; i++) {
expect(exchangeRates[i].denom).to.equal(exchangeRatesJSON[i].denom);
expect(exchangeRates[i].oracleExchangeRateVal.exchangeRate).to.be.a('string').and.to.not.be.empty;
expect(exchangeRates[i].oracleExchangeRateVal.exchangeRate).to.be.a('string').and.to.not.be.empty;
expect(exchangeRates[i].oracleExchangeRateVal.lastUpdateTimestamp).to.exist.and.to.be.gt(0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we check that the other values in oracle_exchange_rate are reasonable values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jewei1997 yea, take a look at this comment above about this #1445 (comment)

});

it("Oracle Twaps", async function () {
const twaps = await oracle.getOracleTwaps(3600);
const twapsLen = twapsJSON.length
expect(twaps.length).to.equal(twapsLen);

for (let i = 0; i < twapsLen; i++) {
expect(twaps[i].denom).to.equal(twapsJSON[i].denom);
expect(twaps[i].twap).to.be.a('string').and.to.not.be.empty;
expect(twaps[i].lookbackSeconds).to.exist.and.to.be.gt(0);
}
});
});
});
});

Expand Down
5 changes: 5 additions & 0 deletions contracts/test/query_oracle_data.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

# This script is used to query oracle exchange rates and twap
seid q oracle exchange-rates -o json > contracts/oracle_exchange_rates.json
seid q oracle twaps 3600 -o json > contracts/oracle_twaps.json
1 change: 1 addition & 0 deletions integration_test/evm_module/hardhat_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- cmd: bash contracts/test/deploy_atom_erc20.sh
- cmd: bash contracts/test/get_validator_address.sh
- cmd: bash contracts/test/send_gov_proposal.sh
- cmd: bash contracts/test/query_oracle_data.sh
verifiers:
- type: eval
expr: RESULT == "0x1"
6 changes: 6 additions & 0 deletions precompiles/common/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/ethereum/go-ethereum/common"
oracletypes "github.com/sei-protocol/sei-chain/x/oracle/types"
)

type BankKeeper interface {
Expand All @@ -31,6 +32,11 @@ type EVMKeeper interface {
GetBaseDenom(ctx sdk.Context) string
}

type OracleKeeper interface {
IterateBaseExchangeRates(ctx sdk.Context, handler func(denom string, exchangeRate oracletypes.OracleExchangeRate) (stop bool))
CalculateTwaps(ctx sdk.Context, lookbackSeconds uint64) (oracletypes.OracleTwaps, error)
}

type WasmdKeeper interface {
Instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.AccAddress, initMsg []byte, label string, deposit sdk.Coins) (sdk.AccAddress, []byte, error)
Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error)
Expand Down
30 changes: 30 additions & 0 deletions precompiles/oracle/Oracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

address constant ORACLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001008;

IOracle constant ORACLE_CONTRACT = IOracle(ORACLE_PRECOMPILE_ADDRESS);

interface IOracle {
// Queries
function getExchangeRates() external view returns (DenomOracleExchangeRatePair[] memory);
function getOracleTwaps(uint64 lookback_seconds) external view returns (OracleTwap[] memory);

// Structs
struct OracleExchangeRate {
string exchangeRate;
string lastUpdate;
int64 lastUpdateTimestamp;
}

struct DenomOracleExchangeRatePair {
string denom;
OracleExchangeRate oracleExchangeRateVal;
}

struct OracleTwap {
string denom;
string twap;
int64 lookbackSeconds;
}
}
1 change: 1 addition & 0 deletions precompiles/oracle/abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[],"name":"getExchangeRates","outputs":[{"components":[{"internalType":"string","name":"denom","type":"string"},{"components":[{"internalType":"string","name":"exchangeRate","type":"string"},{"internalType":"string","name":"lastUpdate","type":"string"},{"internalType":"int64","name":"lastUpdateTimestamp","type":"int64"}],"internalType":"struct IOracle.OracleExchangeRate","name":"oracleExchangeRateVal","type":"tuple"}],"internalType":"struct IOracle.DenomOracleExchangeRatePair[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"lookback_seconds","type":"uint64"}],"name":"getOracleTwaps","outputs":[{"components":[{"internalType":"string","name":"denom","type":"string"},{"internalType":"string","name":"twap","type":"string"},{"internalType":"int64","name":"lookbackSeconds","type":"int64"}],"internalType":"struct IOracle.OracleTwap[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"}]
158 changes: 158 additions & 0 deletions precompiles/oracle/oracle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package oracle

import (
"bytes"
"embed"
"math/big"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
pcommon "github.com/sei-protocol/sei-chain/precompiles/common"
"github.com/sei-protocol/sei-chain/x/oracle/types"
)

const (
GetExchangeRatesMethod = "getExchangeRates"
GetOracleTwapsMethod = "getOracleTwaps"
)

const (
OracleAddress = "0x0000000000000000000000000000000000001008"
)

var _ vm.PrecompiledContract = &Precompile{}

// Embed abi json file to the executable binary. Needed when importing as dependency.
//
//go:embed abi.json
var f embed.FS

func GetABI() abi.ABI {
abiBz, err := f.ReadFile("abi.json")
if err != nil {
panic(err)
}

newAbi, err := abi.JSON(bytes.NewReader(abiBz))
if err != nil {
panic(err)
}
return newAbi
}

type Precompile struct {
pcommon.Precompile
evmKeeper pcommon.EVMKeeper
oracleKeeper pcommon.OracleKeeper
address common.Address

GetExchangeRatesId []byte
GetOracleTwapsId []byte
}

// Define types which deviate slightly from cosmos types (ExchangeRate string vs sdk.Dec)
type OracleExchangeRate struct {
ExchangeRate string `json:"exchangeRate"`
LastUpdate string `json:"lastUpdate"`
LastUpdateTimestamp int64 `json:"lastUpdateTimestamp"`
}

type DenomOracleExchangeRatePair struct {
Denom string `json:"denom"`
OracleExchangeRateVal OracleExchangeRate `json:"oracleExchangeRateVal"`
}

type OracleTwap struct {
Denom string `json:"denom"`
Twap string `json:"twap"`
LookbackSeconds int64 `json:"lookbackSeconds"`
}

func NewPrecompile(oracleKeeper pcommon.OracleKeeper, evmKeeper pcommon.EVMKeeper) (*Precompile, error) {
newAbi := GetABI()

p := &Precompile{
Precompile: pcommon.Precompile{ABI: newAbi},
evmKeeper: evmKeeper,
address: common.HexToAddress(OracleAddress),
oracleKeeper: oracleKeeper,
}

for name, m := range newAbi.Methods {
switch name {
case GetExchangeRatesMethod:
p.GetExchangeRatesId = m.ID
case GetOracleTwapsMethod:
p.GetOracleTwapsId = m.ID
}
}
Comment on lines +84 to +91

Check warning

Code scanning / CodeQL

Iteration over map

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

return p, nil
}

// RequiredGas returns the required bare minimum gas to execute the precompile.
func (p Precompile) RequiredGas(input []byte) uint64 {
methodID := input[:4]

method, err := p.ABI.MethodById(methodID)
if err != nil {
// This should never happen since this method is going to fail during Run
return 0
}

return p.Precompile.RequiredGas(input, p.IsTransaction(method.Name))
}

func (p Precompile) Address() common.Address {
return p.address
}

func (p Precompile) Run(evm *vm.EVM, _ common.Address, input []byte, value *big.Int) (bz []byte, err error) {
ctx, method, args, err := p.Prepare(evm, input)
if err != nil {
return nil, err
}

switch method.Name {
case GetExchangeRatesMethod:
return p.getExchangeRates(ctx, method, args, value)
case GetOracleTwapsMethod:
return p.getOracleTwaps(ctx, method, args, value)
}
return
}

func (p Precompile) getExchangeRates(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, error) {
pcommon.AssertNonPayable(value)
pcommon.AssertArgsLength(args, 0)
exchangeRates := []DenomOracleExchangeRatePair{}
p.oracleKeeper.IterateBaseExchangeRates(ctx, func(denom string, rate types.OracleExchangeRate) (stop bool) {
exchangeRates = append(exchangeRates, DenomOracleExchangeRatePair{Denom: denom, OracleExchangeRateVal: OracleExchangeRate{ExchangeRate: rate.ExchangeRate.String(), LastUpdate: rate.LastUpdate.String(), LastUpdateTimestamp: rate.LastUpdateTimestamp}})
return false
})

return method.Outputs.Pack(exchangeRates)
}

func (p Precompile) getOracleTwaps(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, error) {
pcommon.AssertNonPayable(value)
pcommon.AssertArgsLength(args, 1)
lookbackSeconds := args[0].(uint64)
twaps, err := p.oracleKeeper.CalculateTwaps(ctx, lookbackSeconds)
if err != nil {
return nil, err
}
// Convert twap to string
oracleTwaps := make([]OracleTwap, 0, len(twaps))
for _, twap := range twaps {
oracleTwaps = append(oracleTwaps, OracleTwap{Denom: twap.Denom, Twap: twap.Twap.String(), LookbackSeconds: twap.LookbackSeconds})
}
return method.Outputs.Pack(oracleTwaps)
}

func (Precompile) IsTransaction(string) bool {
return false
}
Loading