Skip to content

Commit

Permalink
Oracle Precompile (#1445)
Browse files Browse the repository at this point in the history
* Oracle Precompile

* Update abi oracle

* Update tests

* Update tests

* Update abi + tests

* integration test query

* Precompile integration tests

* Update type

* Add more loggign

* lint

* Add more checks

* update tests
  • Loading branch information
Kbhat1 authored Mar 26, 2024
1 parent 7faa3ba commit 1af3897
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 0 deletions.
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);
}
});

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
}
}

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

0 comments on commit 1af3897

Please sign in to comment.