Onchain.EVM (onchain_evm v0.4.0)

Copy Markdown View Source

Local EVM simulation powered by revm via Rustler NIF.

Simulates contract execution locally by forking mainnet state from any RPC endpoint. Zero gas cost, zero latency compared to on-chain execution.

Core Use Cases

  • Run hundreds of "what if" scenarios (supply X, borrow Y, price drops Z%)
  • Test liquidation thresholds without on-chain risk
  • Estimate gas usage for transactions before sending
  • Validate contract interactions with state overrides

Error Format

SourceError Shape
EVM execution reverted{:error, {:evm_revert, revert_data_hex}}
EVM execution error{:error, {:evm_error, reason}}
Fork/RPC connection error{:error, {:fork_error, reason}}
RPC request timeout{:error, {:timeout, reason}}
Invalid address input{:error, {:invalid_address, input}}
Invalid hex data input{:error, {:invalid_data, input}}
Invalid RPC URL{:error, {:invalid_rpc_url, reason}}
Invalid value option{:error, {:invalid_value, input}}
Invalid gas_limit option{:error, {:invalid_gas_limit, input}}
Invalid timeout_ms option{:error, {:invalid_timeout_ms, input}}
Invalid state_overrides option{:error, {:invalid_state_overrides, input}}

Timeouts

Each NIF call configures a reqwest::Client with a per-RPC-request timeout. Default is 30 seconds (sufficient for archive-node reads). Pass :timeout_ms to override. Note this caps each individual RPC request, not the aggregate simulation time — a single simulate_transaction may issue many RPC reads for accounts and storage slots.

Functions

FunctionPurpose
simulate_call/3Read-only call simulation → raw hex output
simulate_call!/3Same, raises on error
simulate_transaction/3Full tx simulation → success, gas, output, logs
simulate_transaction!/3Same, raises on error
simulate_batch/2Batch calls on shared fork → list of results
simulate_batch!/2Same, raises on error

API Functions

FunctionArityDescriptionParam Kinds
simulate_batch!2Simulate multiple calls on a shared fork. Raises on error.calls: value, opts: value
simulate_batch2Simulate multiple calls on a single forked EVM state.calls: value, opts: value
simulate_transaction!3Simulate a full transaction. Raises on error.address: value, data: value, opts: value
simulate_transaction3Simulate a full transaction locally, returning gas usage, output, and logs.address: value, data: value, opts: value
simulate_call!3Simulate a read-only contract call. Raises on error.address: value, data: value, opts: value
simulate_call3Simulate a read-only contract call locally using a forked EVM state.address: value, data: value, opts: value

Summary

Types

All possible errors from EVM simulation functions.

EVM log entry from transaction simulation.

Errors from the Rust NIF during EVM execution.

RPC URL validation sub-reasons.

Options for EVM simulation functions.

State overrides to apply before simulation.

Transaction simulation result with gas usage and logs.

Validation errors from Elixir-side input checks.

Functions

Simulate multiple calls on a single forked EVM state.

Simulate multiple calls on a shared fork. Raises on error.

Simulate a read-only contract call locally using a forked EVM state.

Simulate a read-only contract call. Raises on error.

Simulate a full transaction locally, returning gas usage, output, and logs.

Simulate a full transaction. Raises on error.

Types

evm_error()

@type evm_error() :: validation_error() | nif_error()

All possible errors from EVM simulation functions.

log_entry()

@type log_entry() :: %{address: String.t(), topics: [String.t()], data: String.t()}

EVM log entry from transaction simulation.

nif_error()

@type nif_error() ::
  {:evm_revert, String.t()}
  | {:evm_error, String.t()}
  | {:fork_error, String.t()}
  | {:timeout, String.t()}

Errors from the Rust NIF during EVM execution.

rpc_url_reason()

@type rpc_url_reason() ::
  :missing
  | :empty
  | {:not_a_string, term()}
  | {:invalid_scheme, String.t()}
  | {:missing_host, String.t()}

RPC URL validation sub-reasons.

sim_opts()

@type sim_opts() :: [
  rpc_url: String.t(),
  block: non_neg_integer() | String.t(),
  from: String.t() | binary(),
  value: String.t(),
  gas_limit: non_neg_integer(),
  timeout_ms: pos_integer(),
  state_overrides: state_overrides()
]

Options for EVM simulation functions.

state_overrides()

@type state_overrides() :: %{
  optional(String.t()) => %{optional(String.t()) => String.t()}
}

State overrides to apply before simulation.

Keys are 0x-prefixed address strings. Values are maps with string keys: "balance" (hex string), "nonce" (string of integer), "code" (hex string), "storage" (JSON string of slot→value map).

All keys and values must be strings — the NIF decodes them as HashMap<String, String>.

%{
  "0xAddress..." => %{
    "balance" => "0xDE0B6B3A7640000",
    "nonce" => "5",
    "code" => "0x6080...",
    "storage" => ~s({"0x0": "0x1"})
  }
}

tx_result()

@type tx_result() :: %{
  success: boolean(),
  gas_used: non_neg_integer(),
  output: String.t(),
  logs: [log_entry()]
}

Transaction simulation result with gas usage and logs.

validation_error()

@type validation_error() ::
  {:invalid_rpc_url, rpc_url_reason()}
  | {:invalid_address, term()}
  | {:invalid_data, term()}
  | {:invalid_block, term()}
  | {:invalid_value, term()}
  | {:invalid_gas_limit, term()}
  | {:invalid_timeout_ms, term()}
  | {:invalid_state_overrides, term()}

Validation errors from Elixir-side input checks.

Functions

simulate_batch(calls, opts \\ [])

@spec simulate_batch([{String.t() | binary(), String.t()}], sim_opts()) ::
  {:ok, [tx_result()]} | {:error, evm_error()}

Simulate multiple calls on a single forked EVM state.

Parameters

  • calls - List of {address, data} tuples — each address as 0x hex, data as 0x hex calldata (value)
  • opts - Options: :rpc_url (required), :block, :from, :gas_limit, :timeout_ms, :state_overrides (default: [], value)

Returns

List of transaction results, one per call ({:ok, [tx_result()]} | {:error, evm_error()})

# descripex:contract
%{
  params: %{
    calls: %{
      description: "List of {address, data} tuples — each address as 0x hex, data as 0x hex calldata",
      kind: :value
    },
    opts: %{
      default: [],
      description: "Options: :rpc_url (required), :block, :from, :gas_limit, :timeout_ms, :state_overrides",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, [tx_result()]} | {:error, evm_error()}",
    description: "List of transaction results, one per call"
  }
}

simulate_batch!(calls, opts \\ [])

@spec simulate_batch!([{String.t() | binary(), String.t()}], sim_opts()) :: [
  tx_result()
]

Simulate multiple calls on a shared fork. Raises on error.

Parameters

  • calls - List of {address, data} tuples (value)
  • opts - Simulation options (default: [], value)

Returns

List of transaction results ([tx_result()])

# descripex:contract
%{
  params: %{
    calls: %{description: "List of {address, data} tuples", kind: :value},
    opts: %{default: [], description: "Simulation options", kind: :value}
  },
  returns: %{type: "[tx_result()]", description: "List of transaction results"}
}

simulate_call(address, data, opts \\ [])

@spec simulate_call(String.t() | binary(), String.t(), sim_opts()) ::
  {:ok, String.t()} | {:error, evm_error()}

Simulate a read-only contract call locally using a forked EVM state.

Parameters

  • address - Contract address as 0x hex string or 20-byte binary (value)
  • data - 0x-prefixed hex-encoded calldata (from ABI.encode_call) (value)
  • opts - Options: :rpc_url (required), :block, :from, :value, :gas_limit, :timeout_ms, :state_overrides (default: [], value)

Returns

Raw 0x-prefixed hex output, compatible with ABI.decode_response/2 ({:ok, hex_string} | {:error, evm_error()})

# descripex:contract
%{
  params: %{
    data: %{
      description: "0x-prefixed hex-encoded calldata (from ABI.encode_call)",
      kind: :value
    },
    opts: %{
      default: [],
      description: "Options: :rpc_url (required), :block, :from, :value, :gas_limit, :timeout_ms, :state_overrides",
      kind: :value
    },
    address: %{
      description: "Contract address as 0x hex string or 20-byte binary",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, hex_string} | {:error, evm_error()}",
    description: "Raw 0x-prefixed hex output, compatible with ABI.decode_response/2"
  }
}

simulate_call!(address, data, opts \\ [])

@spec simulate_call!(String.t() | binary(), String.t(), sim_opts()) :: String.t()

Simulate a read-only contract call. Raises on error.

Parameters

  • address - Contract address (value)
  • data - 0x-prefixed hex-encoded calldata (value)
  • opts - Simulation options (default: [], value)

Returns

Raw 0x-prefixed hex output (string)

# descripex:contract
%{
  params: %{
    data: %{description: "0x-prefixed hex-encoded calldata", kind: :value},
    opts: %{default: [], description: "Simulation options", kind: :value},
    address: %{description: "Contract address", kind: :value}
  },
  returns: %{type: :string, description: "Raw 0x-prefixed hex output"}
}

simulate_transaction(address, data, opts \\ [])

@spec simulate_transaction(String.t() | binary(), String.t(), sim_opts()) ::
  {:ok, tx_result()} | {:error, evm_error()}

Simulate a full transaction locally, returning gas usage, output, and logs.

Parameters

  • address - Contract address as 0x hex string or 20-byte binary (value)
  • data - 0x-prefixed hex-encoded calldata (value)
  • opts - Options: :rpc_url (required), :block, :from, :value, :gas_limit, :timeout_ms, :state_overrides (default: [], value)

Returns

Transaction result with :success, :gas_used, :output, :logs ({:ok, tx_result()} | {:error, evm_error()})

# descripex:contract
%{
  params: %{
    data: %{description: "0x-prefixed hex-encoded calldata", kind: :value},
    opts: %{
      default: [],
      description: "Options: :rpc_url (required), :block, :from, :value, :gas_limit, :timeout_ms, :state_overrides",
      kind: :value
    },
    address: %{
      description: "Contract address as 0x hex string or 20-byte binary",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, tx_result()} | {:error, evm_error()}",
    description: "Transaction result with :success, :gas_used, :output, :logs"
  }
}

simulate_transaction!(address, data, opts \\ [])

@spec simulate_transaction!(String.t() | binary(), String.t(), sim_opts()) ::
  tx_result()

Simulate a full transaction. Raises on error.

Parameters

  • address - Contract address (value)
  • data - 0x-prefixed hex-encoded calldata (value)
  • opts - Simulation options (default: [], value)

Returns

Transaction result map (tx_result())

# descripex:contract
%{
  params: %{
    data: %{description: "0x-prefixed hex-encoded calldata", kind: :value},
    opts: %{default: [], description: "Simulation options", kind: :value},
    address: %{description: "Contract address", kind: :value}
  },
  returns: %{type: "tx_result()", description: "Transaction result map"}
}