sol_evm

A pure-Dart Ethereum Virtual Machine — plus the opcode table, two-pass assembler, and bytecode utilities the sol_pkgs compiler builds on.

No native toolchain, no FFI, no node: deploy a contract, send transactions to it, inspect the resulting state, and persist the whole chain as JSON, all from Dart.

Features

Execution engine

  • Evm — a local chain with a full account model (nonce, balance, code, storage). State accumulates across transactions and survives a process restart.
    • deploy() / call() / staticCall() / simulate() / estimateGas()
    • CREATE and CREATE2, with address derivation matching mainnet
    • The whole CALL family: CALL, CALLCODE, DELEGATECALL, STATICCALL
    • Nested frames with journaled snapshot/rollback — a REVERT deep in a call tree leaves no trace
    • SELFDESTRUCT under EIP-6780 rules
  • WorldState — accounts, storage, EIP-2929 warm/cold access sets, refund accounting, and JSON persistence via encode() / decode()
  • Interpreter — run one piece of bytecode in isolation, no chain required
  • Gas: intrinsic transaction cost, memory expansion, EIP-2929 cold/warm access, EIP-2200 net SSTORE metering, EIP-3529 refund caps, the EIP-150 63/64 rule
  • Tracing: opcode-level TraceEntry stream for building a step debugger
  • Structured failures: HaltReason distinguishes revert, out-of-gas, invalid opcode, write protection and create failures; Error(string) / Panic(uint256) revert payloads are decoded automatically
  • Hardforks: Hardfork.shanghai and Hardfork.cancun

Bytecode tooling

  • Complete Opcode enum covering Shanghai + Cancun (including TLOAD/TSTORE, MCOPY, BLOBHASH, BLOBBASEFEE, PUSH0), each recording byte value, stack consumed/produced, base gas cost and immediate byte count
  • Assembler for building bytecode programmatically, with two-pass label resolution
  • BytecodeLinker, source maps

Usage

import 'package:sol_evm/sol_evm.dart';

void main() {
  final evm = Evm();
  final alice = addressFromHex('0x1000000000000000000000000000000000000001');
  evm.setBalance(alice, BigInt.from(10).pow(18));

  // Deploy. `bytecode` is initcode — the constructor, as solc emits it.
  final deployment = evm.deploy(from: alice, bytecode: initCode);
  final token = deployment.createdAddress!;

  // Send a transaction; state changes stick.
  final tx = evm.call(from: alice, to: token, data: transferCalldata);
  print('gas ${tx.gasUsed}, ${tx.logs.length} events');
  if (!tx.success) print(tx.failureMessage); // e.g. "reverted: insufficient balance"

  // Read without spending anything.
  final balance = evm.staticCall(to: token, data: balanceOfCalldata);

  // Persist the chain, then pick it back up later.
  final snapshot = evm.state.encode();
  final resumed = Evm(state: WorldState.decode(snapshot));
}

dart run example/main.dart walks the whole loop end to end: fund, deploy, send, inspect, trace, persist, restore.

Tracing

final result = evm.call(from: alice, to: contract, tracer: (entry) => print(entry));
for (final step in result.trace) {
  print('${step.pc}: ${step.opcode?.name} gas=${step.gasLeft} ${step.stack}');
}

Standalone bytecode

final code = (Assembler()
      ..push1(1)
      ..push1(2)
      ..add()
      ..push1(0)
      ..emit(Opcode.MSTORE)
      ..push1(32)
      ..push1(0)
      ..ret())
    .assemble();

final result = const Interpreter().run(code: code, gasLimit: 100000);
print(bytesToBigInt(result.returnData)); // 3

Scope and limitations

This is an execution engine for local development and testing, not a consensus client.

  • No state trie. Accounts live in a map; there are no state or receipt roots, so block hashes cannot be verified against a real chain. BLOCKHASH returns a deterministic pseudo-hash for the last 256 blocks.
  • Precompiles are partial. sha256 (0x02), identity (0x04) and modexp (0x05) are implemented. ecrecover, ripemd160, the bn256 operations, blake2f and point evaluation report an explicit "not implemented" failure rather than returning zeroes that would look like a valid answer.
  • Gas is close, not certified. The schedule follows the Shanghai/Cancun EIPs, but it has not been validated against the ethereum/tests GeneralStateTests vectors. Treat estimates as accurate enough to develop against, not to price a mainnet transaction.
  • No transaction pool, signatures, or blocks. Evm applies transactions directly; signing and RPC live in sol_web3.

Dependencies

  • sol_support — keccak256 and compiler foundations
  • crypto — SHA-256, for the 0x02 precompile

Libraries

sol_evm
EVM opcode table, assembler, bytecode utilities, and a pure-Dart execution engine with a full account/world-state model.