
eevm
An Ethereum Virtual Machine written in Elixir.
eevm runs EVM bytecode, transactions, and blocks in pure Elixir, for every fork from Frontier to Osaka. It is its own implementation, not a wrapper around revm or evmone, and its cryptography is Elixir too, from Keccak-256 to BLS12-381 and KZG. It has no native code: no NIFs and no OTP :crypto.
Status: eevm passes the official Ethereum test suites, apart from the exceptions under Conformance. It is young and its API is not yet stable, so think twice before using it where consensus correctness matters.
Installation
Add eevm to your dependencies in mix.exs; it needs Elixir 1.18 and OTP 27 or later.
def deps do
[{:eevm, "~> 0.1.0"}]
endThe changelog lists what each release adds and changes.
Quick start
Run bytecode
EEVM.run/2 runs code as a message call, without transaction rules. status is :success, :revert, or {:halt, reason}.
# PUSH1 42, PUSH1 0, MSTORE, PUSH1 32, PUSH1 0, RETURN
iex> {:ok, result} = EEVM.run(<<0x60, 42, 0x60, 0, 0x52, 0x60, 32, 0x60, 0, 0xF3>>)
iex> {result.status, :binary.decode_unsigned(result.return_data), result.gas_used}
{:success, 42, 18}Pass tracer: true to get an EIP-3155 trace in result.trace.
Deploy and call a contract
EEVM.create/3 deploys a contract and EEVM.call/4 calls it by address, with no key, nonce, or fees. EEVM.ABI builds the calldata and decodes the return data.
# Runtime code: return the first argument plus 1. Initcode: copy the
# runtime code from its own tail and return it.
iex> runtime = <<0x60, 4, 0x35, 0x60, 1, 0x01, 0x60, 0, 0x52, 0x60, 32, 0x60, 0, 0xF3>>
iex> initcode = <<0x60, 14, 0x60, 12, 0x60, 0, 0x39, 0x60, 14, 0x60, 0, 0xF3>> <> runtime
iex> {:ok, deployed} = EEVM.create(EEVM.Database.InMemory.new(), initcode, caller: 0xA11CE)
iex> calldata = EEVM.ABI.encode_call("increment(uint256)", [41])
iex> {:ok, result} = EEVM.call(deployed.db, deployed.contract_address, calldata, caller: 0xB0B)
iex> EEVM.ABI.decode(["uint256"], result.return_data)
{:ok, [42]}Neither call commits anything: pass result.db to the next call. EEVM.revert_reason/1 decodes a reverted call's Error(string), Panic(uint256), or custom error.
Execute a transaction
EEVM.transact/4 runs a signed transaction end to end: validation, gas, execution, refunds, and fees. A transaction that fails validation returns {:error, reason}; one that reverts or halts returns {:ok, result} with that status.
alias EEVM.{BlockEnv, Config, Database}
alias EEVM.Database.InMemory
alias EEVM.Transaction.{FeeMarket, Signer}
private_key = <<0x46::256>>
{:ok, sender} = Signer.address(private_key)
config = Config.new(:cancun, chain_id: 1)
db = InMemory.new(accounts: %{sender => %{balance: 10 ** 18, nonce: 0, code: <<>>}})
block = BlockEnv.new(number: 1, base_fee: 7, coinbase: 0xC0FFEE)
{:ok, tx} =
%FeeMarket{max_priority_fee_per_gas: 1, max_fee_per_gas: 10, gas_limit: 21_000, to: 0xB0B, value: 1_000}
|> Signer.sign(private_key, config)
{:ok, result} = EEVM.transact(tx, block, db, config: config)
result.receipt
#=> %EEVM.Block.Receipt{tx_type: :eip1559, status: 1, cumulative_gas_used: 21000, ...}
Database.get_balance(result.db, 0xB0B)
#=> 1000The API documentation covers the rest: building and executing blocks with EEVM.Block, your own state backend with EEVM.Database, tracing, simulation from any address, and every error.
Hardforks and EIPs
Pick the rules with config: EEVM.Config.new(:prague, chain_id: 1); the default is :cancun. Every rule is gated on the EIP that introduced it, and EEVM.Config.enabled?/2 says whether an EIP is active.
| Area | Implemented |
|---|---|
| Transactions | Legacy with EIP-155, EIP-2718 envelopes, EIP-2930 access lists, EIP-1559 fee market, EIP-4844 blobs, EIP-7702 set code, EIP-7623 calldata floor, EIP-7825 gas limit cap, EIP-7594 blob limit |
| Execution and gas | Full opcode set through Osaka (including PUSH0, TLOAD/TSTORE, MCOPY, BLOBHASH, BLOBBASEFEE, CLZ), EIP-2929 warm/cold access, EIP-2200 and EIP-3529 SSTORE metering and refunds, EIP-161, EIP-170, EIP-3541, EIP-3651, EIP-3860, EIP-6780 |
| Precompiles | 0x01–0x11 and 0x100: ECRECOVER, SHA256, RIPEMD160, IDENTITY, MODEXP (EIP-198, EIP-2565; EIP-7823 and EIP-7883 from Osaka), alt_bn128 (EIP-196/197), BLAKE2F (EIP-152), KZG point evaluation (EIP-4844), BLS12-381 (EIP-2537), P256VERIFY (EIP-7951, from Osaka) |
| Blocks | RLP block codec, building, header and body validation, EIP-7934 size limit, EIP-4895 withdrawals, EIP-4788 beacon roots, EIP-2935 block hashes, EIP-7685 requests (EIP-6110 deposits, EIP-7002 withdrawals, EIP-7251 consolidations), receipts, logs bloom, MPT state, transaction, and receipt roots |
| Tooling | EIP-3155 tracing, disassembler, custom precompile registration |
Osaka's execution-layer EIPs are implemented. The forks that only change blob parameters (EIP-7892) are not modelled.
Conformance
CI runs both official fixture sources on every push, in the Conformance jobs of ci.yml, and fails on any case that a known-failures list doesn't cover.
- execution-spec-tests (EEST) v5.4.0 is the authoritative suite for current forks:
fixtures_stablethrough Prague, andfixtures_developfor Osaka. ethereum/testsatafed83bis the legacy suite.
| CI job | Cases passed | Known failures | Stale | Skipped fixtures | Notes |
|---|---|---|---|---|---|
| EEST state tests | 44,039 of 44,039 | 0 | 0 | 0 | |
| EEST blockchain tests | 47,563 of 47,563 | 0 | 0 | 7 | Too slow for the CI timeout (#224) |
| EEST Osaka state tests | 19,517 of 19,517 | 0 | 0 | 0 | |
| EEST Osaka blockchain tests | 21,644 of 21,644 | 0 | 0 | 0 | |
| ethereum/tests state tests | 41,652 of 41,652 | 0 | 0 | 2 | Too slow for the CI timeout (#201, #224) |
| ethereum/tests blockchain tests | 633 of 1,168 | 3 | 532 | 0 | Failures: the DAO fork (#183). Stale: Prague fixtures filled before the EIP-7002/EIP-7251 request contracts were required |
Case counts come from CI for these pinned versions, and a skipped fixture's cases aren't counted. The other columns mirror the lists in test/fixtures/, and a test fails when they disagree, so the table can't drift from what CI enforces. Each job's log has the latest counts.
Contributing
Read AGENTS.md for the conventions and checks. Open tasks are in GitHub Issues. mix bench benchmarks the interpreter and precompiles; bench/README.md describes the workloads.