Skip to content
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

Oracle Precompile #1445

Merged
merged 16 commits into from
Mar 26, 2024
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();
Kbhat1 marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Kbhat1 marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
});

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
}
}
Dismissed Show dismissed Hide dismissed

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
Kbhat1 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Kbhat1 marked this conversation as resolved.
Show resolved Hide resolved
return
}

func (p Precompile) getExchangeRates(ctx sdk.Context, method *abi.Method, args []interface{}, value *big.Int) ([]byte, error) {
pcommon.AssertNonPayable(value)
Kbhat1 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading