EEVM — Ethereum Virtual Machine implementation in Elixir.

Execution engine for every fork from Frontier to Osaka: full opcode set, all precompiles (BLS12-381 and KZG in pure Elixir), transaction validation and processing, whole blocks, and MPT-backed state roots. CI runs the official execution-spec-tests and ethereum/tests suites; the README's "Conformance" section lists the results and the known exceptions.

Quick Start

iex> {:ok, result} = EEVM.run(<<0x60, 2, 0x60, 3, 0x01, 0x60, 0, 0x52, 0x60, 32, 0x60, 0, 0xF3>>)
iex> result.status
:success
iex> :binary.decode_unsigned(result.return_data)
5

Module Map

The public API:

Extension points: EEVM.Precompile (custom precompiles), EEVM.Tracer (EIP-3155 traces), EEVM.Block.Processor (custom transaction executors), and EEVM.SystemContracts (genesis setup). Modules grouped under "Internals" in the docs are implementation details and may change in any release.

Errors

A mistake in the arguments your own code passes, such as a hex string where run/2, call/4, or create/3 take bytes, raises ArgumentError, while anything that can arrive from outside, such as the raw signed bytes transact/4 takes, a block, or a database read, returns {:error, reason} with a reason listed in EEVM.Error.

Summary

Functions

Returns an address as the integer eevm uses everywhere: from a hex string (with or without 0x, any case), a 20-byte binary, or an integer.

Decodes a hex string, with or without 0x and in any case, into bytes: the inverse of hex/1 for binaries. Use it on bytecode, calldata, or raw transactions copied from a compiler, explorer, or wallet; run/2, call/4, create/3, and transact/4 take bytes and reject 0x-prefixed hex strings.

Calls the contract at address to with calldata, without a signed transaction: like eth_call, from any :caller.

Deploys a contract by running initcode, without a signed transaction: CREATE from any :caller.

Disassembles bytecode into a human-readable list of instructions.

Returns the first log topic of an event signature, its Keccak-256 hash, as an integer, which is how result.logs carries topics. Anonymous events have no such topic. EEVM.ABI.decode_log/2 matches and decodes a whole log.

Executes EVM bytecode and returns the final machine state.

Renders bytes or a non-negative integer as 0x-prefixed lowercase hex.

Returns the 32-byte Keccak-256 hash of data, Ethereum's hash function (the original Keccak padding, not NIST SHA3-256).

logs(state) deprecated

Returns the logs emitted, in order. Deprecated: use run/2 and read result.logs.

logs_bloom(state) deprecated

Computes the logs bloom of the emitted logs. Deprecated: use EEVM.Block.Bloom.from_logs/1.

Decodes why a call reverted, from its REVERT data or from an EEVM.CallResult or EEVM.TransactionResult.

Runs code as a message call, with no transaction rules: no signature, nonce, intrinsic gas, or fee checks.

Returns the 4-byte ABI selector of a function signature: the first four bytes of its Keccak-256 hash. Write the signature in canonical form, with no spaces or parameter names and full type names (uint256, not uint).

stack_values(state) deprecated

Returns the stack contents (top first). Deprecated: use run/2 with debug: true.

Executes bytecode with an opcode-level tracer attached and returns {final_state, tracer}.

Validates and executes one signed transaction against db in the block environment block: decode, recover the sender, validate, charge gas, execute, refund, and pay the coinbase.

Functions

address(value)

@spec address(String.t() | <<_::160>> | non_neg_integer()) :: non_neg_integer()

Returns an address as the integer eevm uses everywhere: from a hex string (with or without 0x, any case), a 20-byte binary, or an integer.

Raises ArgumentError for anything else.

Examples

iex> EEVM.address("0x00000000000000000000000000000000000000Ff")
255
iex> EEVM.address(<<0xB0B::160>>)
2827

bytes(hex)

@spec bytes(String.t()) :: binary()

Decodes a hex string, with or without 0x and in any case, into bytes: the inverse of hex/1 for binaries. Use it on bytecode, calldata, or raw transactions copied from a compiler, explorer, or wallet; run/2, call/4, create/3, and transact/4 take bytes and reject 0x-prefixed hex strings.

Raises ArgumentError for anything that isn't an even number of hex digits.

Examples

iex> EEVM.bytes("0x6002600301")
<<0x60, 0x02, 0x60, 0x03, 0x01>>
iex> EEVM.bytes("ABcd")
<<0xAB, 0xCD>>
iex> EEVM.bytes("0x")
""

call(db, to, calldata \\ <<>>, opts \\ [])

@spec call(EEVM.Database.t(), non_neg_integer(), binary(), keyword()) ::
  {:ok, EEVM.CallResult.t()} | {:error, EEVM.Error.reason()}

Calls the contract at address to with calldata, without a signed transaction: like eth_call, from any :caller.

The code is loaded from db, following an EIP-7702 delegation, and runs as to, so it sees to's balance and storage; a precompile address runs the precompile. :value moves from the caller to to first, and the addresses EIP-2929 starts warm (caller, to, precompiles, and from Shanghai the coinbase) are warm.

No transaction rules apply: no signature, nonce, intrinsic gas, or fees. result.gas_used is execution gas only, without the 21,000 a transaction would add.

Options

  • :caller — CALLER and ORIGIN (default 0)
  • :value — wei sent with the call (default 0)
  • :gas — the gas limit (default 30,000,000)
  • :config (or the :hardfork shorthand), :block, :tracer, and :debug, as for run/2

Any other option raises ArgumentError.

Returns {:ok, %EEVM.CallResult{}} however execution ends; check result.status, and see revert_reason/1 for a revert. Nothing is committed: on success result.db holds the writes in its overlay, so pass it to the next call, or commit it with EEVM.Database.commit/1. Returns {:error, :insufficient_balance} when :value exceeds the caller's balance, or {:error, {:database, reason}} when the backend fails a read.

Example

iex> # Runtime code: return storage slot 0 as a word.
iex> code = <<0x60, 0, 0x54, 0x60, 0, 0x52, 0x60, 32, 0x60, 0, 0xF3>>
iex> db = EEVM.Database.InMemory.new(accounts: %{0xC0DE => %{code: code}}, storage: %{0xC0DE => %{0 => 42}})
iex> {:ok, result} = EEVM.call(db, 0xC0DE, <<>>, caller: 0xA11CE)
iex> {result.status, :binary.decode_unsigned(result.return_data)}
{:success, 42}

create(db, initcode, opts \\ [])

@spec create(EEVM.Database.t(), binary(), keyword()) ::
  {:ok, EEVM.CallResult.t()} | {:error, EEVM.Error.reason()}

Deploys a contract by running initcode, without a signed transaction: CREATE from any :caller.

The caller's nonce is incremented and the new address derived from its value before the increment, as for a contract-creation transaction, so repeated deploys get new addresses. :value moves to the new account, the initcode runs there, and the code it returns is installed under the EIP-170 size limit, EIP-3541, and the code-deposit charge. result.contract_address is the new address, or nil when the deployment failed.

Takes the same options as call/4, and like it applies no transaction rules and commits nothing: on success result.db holds the new account in its overlay; on failure it is db unchanged, nonce included. From Shanghai, initcode over 49,152 bytes returns {:error, :initcode_too_large} (EIP-3860).

Example

iex> # Initcode: copy the 11-byte runtime code from the call in `call/4`
iex> # and return it.
iex> runtime = <<0x60, 0, 0x54, 0x60, 0, 0x52, 0x60, 32, 0x60, 0, 0xF3>>
iex> initcode = <<0x60, 11, 0x60, 12, 0x60, 0, 0x39, 0x60, 11, 0x60, 0, 0xF3>> <> runtime
iex> {:ok, result} = EEVM.create(EEVM.Database.InMemory.new(), initcode, caller: 0xA11CE)
iex> EEVM.Database.get_code(result.db, result.contract_address) == runtime
true

disassemble(bytecode)

@spec disassemble(binary()) :: [{non_neg_integer(), String.t(), String.t() | nil}]

Disassembles bytecode into a human-readable list of instructions.

Example

iex> EEVM.disassemble(<<0x60, 0x01, 0x60, 0x02, 0x01, 0x00>>)
[{0, "PUSH1", "0x01"}, {2, "PUSH1", "0x02"}, {4, "ADD", nil}, {5, "STOP", nil}]

event_topic(signature)

@spec event_topic(String.t()) :: non_neg_integer()

Returns the first log topic of an event signature, its Keccak-256 hash, as an integer, which is how result.logs carries topics. Anonymous events have no such topic. EEVM.ABI.decode_log/2 matches and decodes a whole log.

Example

iex> EEVM.event_topic("Transfer(address,address,uint256)") |> EEVM.hex()
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

execute(bytecode, opts \\ [])

This function is deprecated. Use EEVM.run/2.
@spec execute(
  binary(),
  keyword()
) :: EEVM.Interpreter.MachineState.t()

Executes EVM bytecode and returns the final machine state.

Deprecated: use run/2, which returns an EEVM.CallResult. This takes the options :gas, :hardfork, :config, :db, :block, :tx, and :contract.

hex(bytes)

@spec hex(binary() | non_neg_integer()) :: String.t()

Renders bytes or a non-negative integer as 0x-prefixed lowercase hex.

A binary keeps every byte, so pass a hash or <<address::160>> to keep its width; an integer renders as a JSON-RPC quantity, without leading zeros.

Examples

iex> EEVM.hex(<<0, 1, 0xAB>>)
"0x0001ab"
iex> EEVM.hex(255)
"0xff"
iex> EEVM.hex(0)
"0x0"

keccak256(data)

@spec keccak256(binary()) :: <<_::256>>

Returns the 32-byte Keccak-256 hash of data, Ethereum's hash function (the original Keccak padding, not NIST SHA3-256).

Example

iex> EEVM.keccak256("") |> EEVM.hex()
"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"

logs(state)

This function is deprecated. Use EEVM.run/2 and read result.logs.
@spec logs(EEVM.Interpreter.MachineState.t()) :: [map()]

Returns the logs emitted, in order. Deprecated: use run/2 and read result.logs.

logs_bloom(state)

This function is deprecated. Use EEVM.Block.Bloom.from_logs/1.

Computes the logs bloom of the emitted logs. Deprecated: use EEVM.Block.Bloom.from_logs/1.

revert_reason(data)

@spec revert_reason(binary() | EEVM.CallResult.t() | EEVM.TransactionResult.t()) ::
  {:error_string, String.t()}
  | {:panic, non_neg_integer()}
  | {:custom, <<_::32>>, binary()}
  | {:raw, binary()}
  | nil

Decodes why a call reverted, from its REVERT data or from an EEVM.CallResult or EEVM.TransactionResult.

Returns:

  • {:error_string, message} for Solidity's Error(string) (selector 0x08c379a0), what require(cond, "message") and revert("message") produce
  • {:panic, code} for Panic(uint256) (selector 0x4e487b71), such as 0x01 for a failed assert, 0x11 for arithmetic overflow, and 0x12 for division by zero
  • {:custom, selector, args} for any other payload of four bytes or more, such as a Solidity custom error (decode its args with EEVM.ABI.decode/2) or a malformed Error(string)
  • {:raw, data} for one to three bytes
  • nil for no data, or for a result whose status isn't :revert

Examples

iex> data = EEVM.ABI.encode_call("Error(string)", ["nope!"])
iex> EEVM.revert_reason(data)
{:error_string, "nope!"}

iex> EEVM.revert_reason(<<0x4E487B71::32, 0x12::256>>)
{:panic, 0x12}

iex> {:ok, result} = EEVM.run(<<0x60, 0, 0x60, 0, 0xFD>>)
iex> {result.status, EEVM.revert_reason(result)}
{:revert, nil}

run(code, opts \\ [])

@spec run(
  binary(),
  keyword()
) :: {:ok, EEVM.CallResult.t()} | {:error, EEVM.Error.reason()}

Runs code as a message call, with no transaction rules: no signature, nonce, intrinsic gas, or fee checks.

Use it to test bytecode or to probe a contract eth_call-style. To run a signed transaction, use transact/4.

Options

  • :config — the EEVM.Config to run under, or :hardfork for the shorthand EEVM.Config.new(hardfork) (default :cancun)
  • :gas — the gas limit (default 1,000,000)
  • :db — the EEVM.Database (default: an empty EEVM.Database.InMemory)
  • :block and :tx — the EEVM.BlockEnv and EEVM.TxEnv the code sees
  • :address, :caller, :value, :calldata — the executing account, CALLER, CALLVALUE, and the call's input (defaults 0, 0, 0, <<>>). :address is required with :db: it is the account whose balance and storage the code sees. To call a contract already in :db, use call/4, which loads its code for you.
  • :tracer — true or an EEVM.Tracer to record an EIP-3155 trace, returned as result.trace
  • :debug — true to return the final stack as result.stack

Any other option, or :db without :address, raises ArgumentError.

Returns {:ok, %EEVM.CallResult{}} however execution ends; check result.status. Writes stay in result.db's overlay and are listed in result.changes; nothing is committed to the backend. A backend read failure returns {:error, {:database, reason}}.

Examples

iex> {:ok, result} = EEVM.run(<<0x60, 0x0A, 0x60, 0x14, 0x01, 0x00>>, debug: true)
iex> {result.status, result.stack, result.gas_used}
{:success, [30], 9}

iex> {:ok, result} = EEVM.run(<<0xFE>>, gas: 100)
iex> {result.status, result.gas_left}
{{:halt, :invalid_opcode}, 0}

selector(signature)

@spec selector(String.t()) :: <<_::32>>

Returns the 4-byte ABI selector of a function signature: the first four bytes of its Keccak-256 hash. Write the signature in canonical form, with no spaces or parameter names and full type names (uint256, not uint).

Prepend it to the ABI-encoded arguments to build calldata, or let EEVM.ABI.encode_call/2 do both.

Example

iex> EEVM.selector("transfer(address,uint256)")
<<0xA9, 0x05, 0x9C, 0xBB>>

stack_values(state)

This function is deprecated. Use EEVM.run/2 with debug: true and read result.stack.
@spec stack_values(EEVM.Interpreter.MachineState.t()) :: [non_neg_integer()]

Returns the stack contents (top first). Deprecated: use run/2 with debug: true.

trace(bytecode, opts \\ [])

This function is deprecated. Use EEVM.run/2 with tracer: true.

Executes bytecode with an opcode-level tracer attached and returns {final_state, tracer}.

Deprecated: use run(code, tracer: true), which returns the trace as result.trace.

transact(tx, block, db, opts \\ [])

Validates and executes one signed transaction against db in the block environment block: decode, recover the sender, validate, charge gas, execute, refund, and pay the coinbase.

tx is a transaction struct (see EEVM.Transaction) or its raw signed bytes. Options are :config (or the :hardfork shorthand) and :tracer, as for run/2, and details: true, which returns a rejection as {:error, %EEVM.TransactionError{}} with the values involved, such as the transaction's and the account's nonce.

For simulation, tx may instead be an unsigned EEVM.TxEnv, which impersonates its origin: no signature is needed or recovered, and every other rule applies, so a wrong nonce or a sender that can't pay is still rejected. The config's simulation switches (see EEVM.Config) relax those rules for tooling; never use either for consensus. Signed transactions are unaffected.

Returns {:ok, %EEVM.TransactionResult{}} for any transaction that passes validation, including reverted and halted ones, or {:error, reason} with a reason from EEVM.Error, leaving db unchanged.

Example

iex> tx = %EEVM.Transaction.Legacy{nonce: 5, gas_price: 10, gas_limit: 21_000, to: 0xB0B}
iex> {:ok, tx} = EEVM.Transaction.Signer.sign(tx, <<0x46::256>>, EEVM.Config.new(:cancun))
iex> db = EEVM.Database.InMemory.new()
iex> EEVM.transact(tx, EEVM.BlockEnv.new(), db)
{:error, :nonce_too_high}
iex> {:error, error} = EEVM.transact(tx, EEVM.BlockEnv.new(), db, details: true)
iex> {error.reason, error.details.tx_nonce, error.details.state_nonce}
{:nonce_too_high, 5, 0}

Impersonating 0xA11CE with an unsigned EEVM.TxEnv:

iex> db = EEVM.Database.InMemory.new(accounts: %{0xA11CE => %{balance: 10 ** 18}})
iex> tx = %EEVM.TxEnv{origin: 0xA11CE, to: 0xB0B, value: 1_000, gas_limit: 21_000, gas_price: 10}
iex> {:ok, result} = EEVM.transact(tx, EEVM.BlockEnv.new(), db)
iex> {result.status, result.sender, EEVM.Database.get_balance(result.db, 0xB0B)}
{:success, 0xA11CE, 1000}
iex> EEVM.transact(%{tx | nonce: 1}, EEVM.BlockEnv.new(), db)
{:error, :nonce_too_high}