Skip to content
Draft
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
9 changes: 9 additions & 0 deletions injective-chain/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ import (
"github.com/InjectiveLabs/injective-core/injective-chain/modules/evm"
evmkeeper "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/keeper"
bankpc "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles/bank"
cosmwasmpc "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles/cosmwasm"
exchangepc "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles/exchange"
oraclepc "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles/oracle"
stakingpc "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles/staking"
Expand Down Expand Up @@ -1191,6 +1192,14 @@ func (app *InjectiveApp) initKeepers(authority string, cfg appconfig.Config, was
storetypes.TransientGasConfig(),
)
},
func(_ sdk.Context, _ ethparams.Rules) vm.PrecompiledContract {
// referencing &app.WasmKeeper is safe here: this generator only runs at
// tx time, after WasmKeeper is initialized further below in NewInjectiveApp.
return cosmwasmpc.NewContract(
&app.WasmKeeper,
storetypes.TransientGasConfig(),
)
},
},
cfg.EVM.EnableGRPCTracing,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

address constant COSMWASM_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000068;

ICosmwasm constant COSMWASM_CONTRACT = ICosmwasm(COSMWASM_PRECOMPILE_ADDRESS);

/// @title CosmWasm query precompile
/// @notice Read-only access to CosmWasm contracts from the EVM. Query-only: no execute/instantiate.
interface ICosmwasm {
/// @notice Smart-query a CosmWasm contract.
/// @param contractAddress bech32 (inj1...) address of the CosmWasm contract
/// @param req JSON query message, ABI-encoded as bytes
/// @return response JSON response bytes; reverts if the contract does not exist or the query errors
function query(string calldata contractAddress, bytes calldata req)
external
view
returns (bytes memory response);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

194 changes: 194 additions & 0 deletions injective-chain/modules/evm/precompiles/cosmwasm/cosmwasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package cosmwasm

import (
"context"
"errors"
"math"

errorsmod "cosmossdk.io/errors"
storetypes "cosmossdk.io/store/types"
"github.com/InjectiveLabs/metrics/v2"
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"

"github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles"
bindings "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles/bindings/cosmos/precompile/cosmwasm"
precomptypes "github.com/InjectiveLabs/injective-core/injective-chain/modules/evm/precompiles/types"
)

const (
// QueryMethodName is the generic read-only smart query:
// query(string contractAddress, bytes req) view returns (bytes response)
QueryMethodName = "query"

// QueryBaseGas is the flat EVM gas charged per call (plus a per-byte calldata cost).
//
// This is NOT the safety bound: actual wasm compute is hard-capped per call by the wasm module's
// QueryGasLimit (see runQuery) — the same limit wasmd already exposes to the public internet for
// free via gRPC — so any positive fee is already stricter than the status quo and there is no DoS
// to price against. It is therefore a spam-deterrent / consistency knob, set to align with the
// heaviest existing precompile methods (cf. exchange's ~200-250k). Tunable; a candidate for a
// governance param.
QueryBaseGas uint64 = 200_000
)

var (
cosmwasmABI abi.ABI
cosmwasmContractAddress = common.HexToAddress("0x0000000000000000000000000000000000000068")
gasRequiredByMethod = map[[4]byte]uint64{}
)

var (
ErrPrecompilePanic = errors.New("cosmwasm precompile panic")
ErrContractNotFound = errors.New("cosmwasm contract not found")
ErrQueryOutOfGas = errors.New("cosmwasm query out of gas")
)

func init() {
if err := cosmwasmABI.UnmarshalJSON([]byte(bindings.CosmwasmModuleMetaData.ABI)); err != nil {
panic(err)
}
for name, gas := range map[string]uint64{
QueryMethodName: QueryBaseGas,
} {
method, ok := cosmwasmABI.Methods[name]
if !ok {
panic(name + " method not found in ABI")
}
var id [4]byte
copy(id[:], method.ID[:4])
gasRequiredByMethod[id] = gas
}
}

// WasmViewKeeper is the read-only subset of wasmkeeper.Keeper this precompile needs.
// *wasmkeeper.Keeper satisfies it (all methods have value receivers).
type WasmViewKeeper interface {
QuerySmart(ctx context.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error)
HasContractInfo(ctx context.Context, contractAddr sdk.AccAddress) bool
QueryGasLimit() storetypes.Gas
}

type Contract struct {
wasmKeeper WasmViewKeeper
kvGasConfig storetypes.GasConfig
}

func NewContract(wasmKeeper WasmViewKeeper, kvGasConfig storetypes.GasConfig) vm.PrecompiledContract {
return &Contract{
wasmKeeper: wasmKeeper,
kvGasConfig: kvGasConfig,
}
}

func (*Contract) ABI() abi.ABI { return cosmwasmABI }
func (*Contract) Address() common.Address { return cosmwasmContractAddress }
func (*Contract) Name() string { return "INJ_COSMWASM" }

func (c *Contract) RequiredGas(input []byte) uint64 {
if len(input) < 4 {
return 0
}
// Reject oversized calldata before ABI decoding.
if len(input) > precomptypes.MAX_ABI_ENCODED_CALLDATA_LENGTH {
return math.MaxUint64
}

baseCost := uint64(len(input)) * c.kvGasConfig.WriteCostPerByte
var methodID [4]byte
copy(methodID[:], input[:4])
if methodGas, ok := gasRequiredByMethod[methodID]; ok {
return methodGas + baseCost
}
return baseCost
}

func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) {
res, err := c.execute(evm, contract, readonly)
if err != nil {
return precomptypes.RevertReasonAndError(err)
}
return res, nil
}

func (c *Contract) execute(evm *vm.EVM, contract *vm.Contract, _ bool) (output []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = errorsmod.Wrapf(ErrPrecompilePanic, "%v", r)
output = nil
}
}()

method, err := cosmwasmABI.MethodById(contract.Input[:4])
if err != nil {
return nil, err
}

stateDB, ok := evm.StateDB.(precompiles.ExtStateDB)
if !ok {
return nil, errors.New("state DB does not implement ExtStateDB")
}
defer func(origCtx sdk.Context) { *stateDB.ContextPtr() = origCtx }(stateDB.Context())
defer stateDB.Meter().FuncTiming(stateDB.ContextPtr(), "execute",
metrics.Tag("svc", "cosmwasmpc"), metrics.Tag("method", method.Name))()

args, err := method.Inputs.Unpack(contract.Input[4:])
if err != nil {
return nil, errors.New("fail to unpack input arguments")
}

// Read-only: run against a cache context; nothing is committed back to state.
ctx := stateDB.CacheContext()
switch method.Name {
case QueryMethodName:
return c.query(ctx, method, args)
}
return nil, errors.New("unknown method")
}

// query(string contractAddress, bytes req) view returns (bytes response)
func (c *Contract) query(ctx sdk.Context, m *abi.Method, args []any) ([]byte, error) {
addrStr, err := precomptypes.CastString(args[0])
if err != nil {
return nil, err
}
req, ok := args[1].([]byte)
if !ok {
return nil, errors.New("could not cast req to bytes")
}
res, err := c.runQuery(ctx, addrStr, req)
if err != nil {
return nil, err
}
return m.Outputs.Pack(res)
}

// runQuery bounds and executes a read-only smart query. The EVM executes under an infinite
// Cosmos gas meter, so we install a fresh bounded meter sized to the wasm module's QueryGasLimit
// — exactly what wasmd's own GrpcQuerier does — otherwise the query would be unbounded. A wasm
// out-of-gas surfaces as a Cosmos ErrorOutOfGas panic, which we recover into a clean revert so it
// can never escape and halt the chain.
func (c *Contract) runQuery(ctx sdk.Context, addrStr string, req []byte) (out []byte, err error) {
contractAddr, err := sdk.AccAddressFromBech32(addrStr)
if err != nil {
return nil, err
}
if !c.wasmKeeper.HasContractInfo(ctx, contractAddr) {
return nil, ErrContractNotFound
}

qCtx := ctx.WithGasMeter(storetypes.NewGasMeter(c.wasmKeeper.QueryGasLimit()))
defer func() {
if r := recover(); r != nil {
if _, ok := r.(storetypes.ErrorOutOfGas); ok {
out, err = nil, ErrQueryOutOfGas
return
}
panic(r) // real panic: re-raise to the outer recover in execute()
}
}()

return c.wasmKeeper.QuerySmart(qCtx, contractAddr, req)
}
109 changes: 109 additions & 0 deletions injective-chain/modules/evm/precompiles/cosmwasm/cosmwasm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cosmwasm

import (
"bytes"
"context"
"math"
"testing"

storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)

// mockWasmKeeper is a stand-in for wasmkeeper.Keeper covering the read-only surface the
// precompile uses. It lets us exercise runQuery without a full app + store.
type mockWasmKeeper struct {
has bool
gasLimit storetypes.Gas
querySmart func(ctx context.Context, addr sdk.AccAddress, req []byte) ([]byte, error)
}

func (m mockWasmKeeper) HasContractInfo(_ context.Context, _ sdk.AccAddress) bool { return m.has }
func (m mockWasmKeeper) QueryGasLimit() storetypes.Gas { return m.gasLimit }
func (m mockWasmKeeper) QuerySmart(ctx context.Context, addr sdk.AccAddress, req []byte) ([]byte, error) {
return m.querySmart(ctx, addr, req)
}

func newTestCtx() sdk.Context {
return sdk.Context{}.WithGasMeter(storetypes.NewGasMeter(10_000_000))
}

func bech32Of(b byte) string {
return sdk.AccAddress(bytes.Repeat([]byte{b}, 20)).String()
}

func TestMetadata(t *testing.T) {
c := NewContract(mockWasmKeeper{}, storetypes.TransientGasConfig())
require.Equal(t, common.HexToAddress("0x0000000000000000000000000000000000000068"), c.Address())
require.Contains(t, cosmwasmABI.Methods, QueryMethodName)
require.Len(t, cosmwasmABI.Methods, 1, "precompile exposes only the generic query method")
}

func TestRequiredGas(t *testing.T) {
c := &Contract{wasmKeeper: mockWasmKeeper{}, kvGasConfig: storetypes.TransientGasConfig()}

require.Equal(t, uint64(0), c.RequiredGas([]byte{0x01}), "sub-selector input is free")

oversized := make([]byte, 10_001)
require.Equal(t, uint64(math.MaxUint64), c.RequiredGas(oversized), "oversized calldata is rejected")

sel := cosmwasmABI.Methods[QueryMethodName].ID[:4]
input := append(append([]byte{}, sel...), bytes.Repeat([]byte{0x00}, 64)...)
want := QueryBaseGas + uint64(len(input))*storetypes.TransientGasConfig().WriteCostPerByte
require.Equal(t, want, c.RequiredGas(input))
}

func TestRunQuery_HappyPath(t *testing.T) {
expected := []byte(`{"count":42}`)
c := &Contract{
wasmKeeper: mockWasmKeeper{
has: true,
gasLimit: 3_000_000,
querySmart: func(_ context.Context, _ sdk.AccAddress, req []byte) ([]byte, error) {
require.Equal(t, `{"count":{}}`, string(req))
return expected, nil
},
},
kvGasConfig: storetypes.TransientGasConfig(),
}
out, err := c.runQuery(newTestCtx(), bech32Of(0x01), []byte(`{"count":{}}`))
require.NoError(t, err)
require.Equal(t, expected, out)
}

func TestRunQuery_ContractNotFound(t *testing.T) {
c := &Contract{
wasmKeeper: mockWasmKeeper{has: false, gasLimit: 3_000_000},
kvGasConfig: storetypes.TransientGasConfig(),
}
_, err := c.runQuery(newTestCtx(), bech32Of(0x01), []byte(`{}`))
require.ErrorIs(t, err, ErrContractNotFound)
}

func TestRunQuery_InvalidAddress(t *testing.T) {
c := &Contract{
wasmKeeper: mockWasmKeeper{has: true, gasLimit: 3_000_000},
kvGasConfig: storetypes.TransientGasConfig(),
}
_, err := c.runQuery(newTestCtx(), "not-a-bech32-address", []byte(`{}`))
require.Error(t, err)
}

func TestRunQuery_OutOfGasRecovered(t *testing.T) {
c := &Contract{
wasmKeeper: mockWasmKeeper{
has: true,
gasLimit: 1_000_000,
querySmart: func(ctx context.Context, _ sdk.AccAddress, _ []byte) ([]byte, error) {
// Simulate an expensive query overrunning the installed gas meter.
sdk.UnwrapSDKContext(ctx).GasMeter().ConsumeGas(2_000_000, "boom")
return []byte("unreached"), nil
},
},
kvGasConfig: storetypes.TransientGasConfig(),
}
_, err := c.runQuery(newTestCtx(), bech32Of(0x01), []byte(`{}`))
require.ErrorIs(t, err, ErrQueryOutOfGas, "wasm OOG must be recovered into a clean error, not a panic")
}
20 changes: 20 additions & 0 deletions injective-chain/modules/evm/precompiles/cosmwasm/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Package cosmwasm provides the CosmWasm query EVM precompile.
//
// The precompile is deployed at address 0x0000000000000000000000000000000000000068 and exposes
// read-only access to CosmWasm contracts from the EVM. It intentionally supports queries ONLY —
// there is no execute/instantiate path — so it cannot mutate Wasm or EVM state and carries no
// cross-VM reentrancy or atomicity surface.
//
// Method:
//
// query(string contractAddress, bytes req) view returns (bytes response)
// Generic smart query. `req` is the JSON query message; `response` is the JSON result bytes.
//
// The precompile is intentionally a generic primitive: it exposes no contract-type-specific helpers.
// Callers decode the JSON response on the Solidity side (e.g. a CW721 owner_of library). On Injective
// an account's inj1 and 0x forms are the same 20 bytes, so an owner returned by a query maps directly
// to an EVM address.
//
// Gas: actual wasm compute is bounded by the wasm module's QueryGasLimit via a freshly installed
// gas meter (mirroring wasmd's GrpcQuerier); an out-of-gas is recovered into a clean revert.
package cosmwasm