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
| Source | Error 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
| Function | Purpose |
|---|---|
simulate_call/3 | Read-only call simulation → raw hex output |
simulate_call!/3 | Same, raises on error |
simulate_transaction/3 | Full tx simulation → success, gas, output, logs |
simulate_transaction!/3 | Same, raises on error |
simulate_batch/2 | Batch calls on shared fork → list of results |
simulate_batch!/2 | Same, raises on error |
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
simulate_batch! | 2 | Simulate multiple calls on a shared fork. Raises on error. | calls: value, opts: value |
simulate_batch | 2 | Simulate multiple calls on a single forked EVM state. | calls: value, opts: value |
simulate_transaction! | 3 | Simulate a full transaction. Raises on error. | address: value, data: value, opts: value |
simulate_transaction | 3 | Simulate a full transaction locally, returning gas usage, output, and logs. | address: value, data: value, opts: value |
simulate_call! | 3 | Simulate a read-only contract call. Raises on error. | address: value, data: value, opts: value |
simulate_call | 3 | Simulate 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
@type evm_error() :: validation_error() | nif_error()
All possible errors from EVM simulation functions.
EVM log entry from transaction simulation.
@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.
@type rpc_url_reason() :: :missing | :empty | {:not_a_string, term()} | {:invalid_scheme, String.t()} | {:missing_host, String.t()}
RPC URL validation sub-reasons.
@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 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"})
}
}
@type tx_result() :: %{ success: boolean(), gas_used: non_neg_integer(), output: String.t(), logs: [log_entry()] }
Transaction simulation result with gas usage and logs.
@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
@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 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"}
}
@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 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"}
}
@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 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"}
}