Solenoid YEVM

Lessons from Building an
Async-First, Wasm-Native EVM in Rust


Sergey Melnychuk  ·  RustMeet 2026  ·  Kraków

⚠️ This talk is NOT about blockchain


It's about Rust — async, WebAssembly, and implementing a complex spec from scratch.


No crypto wallet required 🦀

Agenda

  • About me
  • Beerus → Solenoid → YEVM
  • What? - EVM
  • Why? - wasm, async, observability
  • How? - spec, A/B, trial & error
  • Solenoid
  • YEVM
  • Summary
  • Q&A

About me

Trifecta


Project Goal Result
Beerus StarkNet light client, ready for integration with wallets Done. Used in ETH-STRK bridge. Now closed.
Solenoid Observable async-first wasm-ready EVM from scratch Done. Works too slow. Now archived.
YEVM Observable and fast async + wasm EVM, learn from Solenoid Done. It works.

What? - EVM

YEVM

  • Yet
  • another
  • EVM
  • implementation

Naming things is hard.

What? - EVM

  • Ethereum VM — the runtime for smart contracts
  • Stack-based, 256-bit word, flat memory
  • Deterministic: same (input, state) → same output
  • Specified in the Yellow Paper and EIPs


Why build a new one in 2026?

Why? - wasm, async, observability

  • Production EVMs (geth, revm, evmone) are hyper-optimized and hard to audit
  • No clean wasm-native, async-friendly implementation existed
  • Target: browser-based tooling — IDEs, light clients, simulations
  • Goal: deep understanding of how EVM works "under the hood"

Why? - WebAssembly

  • Runs in the browser — no backend, no install
  • wasm32-unknown-unknown: no OS, no stdlib
  • Same code runs natively and in the browser
  • Portable simulation at the edge
    • wallets
    • IDEs
    • light clients

Why? - async

  • EVM needs external data: storage slots, account code, balances
  • In the browser those fetches are always async (fetch, XHR)
  • Sync EVM will fail to run in the browser (blocking the main thread)
  • Async-first design makes the browser a first-class target

Why? - observability

  • Production EVMs are black boxes — input in, output out
  • Solenoid exposes every opcode, stack frame, memory write, storage access
  • Useful for: debugging, education, security auditing, simulation UI
  • Though full traces might be too much data to handle in browser effectively

Why? - security

  • February 2025: $1.5B stolen from ByBit — largest crypto hack ever
  • Root cause: malicious transaction looked safe in the UI, but wasn't
  • A browser-native EVM could have simulated the full execution before signing
  • Side effects, storage changes, token transfers — all visible before broadcast

Simulation could have prevented the ByBit hack!

How? - overview

  • 📄 Spec — implement the Yellow Paper + EIPs
  • ⚖️ A/B — compare step-by-step against revm
  • 🔁 Trial & error — run real mainnet blocks

How? - spec

  • Yellow Paper is the ground truth — but it's dense and occasionally wrong
  • EIPs override the Paper — and sometimes contradict each other
  • Gas rules have many special cases: SSTORE, CALL, warm/cold access
  • Ethereum GeneralStateTests: ~18k+ test cases across all opcodes and EIPs
  • Getting to 99%+ took fixing multiple bugs — each one a spec rabbit hole

How? - A/B against revm

  • The revm trace is the source of truth*
  • Run the same tx through YEVM and revm
  • Compare every step: opcode, PC, gas, ...
  • Step mismatch → implementations diverge
  • Gas + Trace + State match → peace of mind

How? - trial & error

  • Replay real mainnet blocks — 10, then 100, then 300, then 1000
  • Each milestone exposed new bugs: EIP-7702, CREATE2 etc
  • Failed txs go into a queue, get fixed, re-replayed
  • Fail, Debug, Fix, Replay - Rinse - Repeat

How? - ...

  • 🤯 why would anyone send ETH to 0x0?
  • 🙃 why is this address/cell cold/hot?
  • 🫠 intrinsic gas is capped by floor
  • 😠 refund is capped by constant factor
  • 😩 63/64 rule for *CALL gas makes a mess
  • 😵‍💫 revm offers very limited insights really
  • 🤔 revert impl was tricky to debug & test
  • ...

Solenoid: async-first design

  • External calls (storage, code) are async trait methods
  • Portable: compiles to wasm32-unknown-unknown
  • Observable: every state transition is inspectable


  // simplified
  async fn execute(&mut self, opcode: u8) -> Result<()> {
      match opcode {
          SLOAD => {
              let key = self.stack.pop()?;
              let val = self.storage.load(key).await?;  // async fetch
              self.stack.push(val)
          }
          // ...
      }
  }
    

The async tax

  • Each .await is a yield point — overhead adds up
  • Codegen for deeply nested async is expensive
  • Result: 10–100× slower than revm

But for browser use cases — latency hides the cost:

  • Network round-trips dominate
  • User doesn't notice 10ms vs 100ms
  • Wasm portability is worth the tax

Solenoid: fun facts 🎉

  • 🦄 UniSwap quoter was first external call
  • 
      📊 QuoterV2 Results:
        💰 Amount Out: 1 WETH for 3943.532812 USDC
        📊 Price After (WETH/USDC): 3955.222269012662
        🎯 Initialized Ticks Crossed: 1
        ⛽ Gas Estimate: 84919
      ✅ Transaction executed successfully!
      🔄 Reverted: false
      ⛽ Gas used: 123290
        
  • 🤖 All precompiles implemented by Claude Code
  • 📊 Aztec L2 4.5M gas txs: 55GB of opcode traces

Lessons learned from Solenoid

  • The Yellow Paper has many subtle edge cases
  • Gas accounting is hard (looking at you SSTORE)
  • Debug info is very expensive — allocator round-trips add up fast in a hot loop
  • Async in hot paths is still very expensive
  • Key insight: the VM yields at every .await — what if it didn't?

Lessons learned from Solenoid

Designing and evolving such complex thing as EVM is hard

YEVM: make yield part of the design

  • Solenoid yields to the executor on async fetches
  • YEVM returns fetch request, sync execution
  • Execution and fetching are decoupled:
    • opcode handler is sync (no .awaits)
    • executor is async (handles fetches)
    • but deals with "pending" side-effects

YEVM: model


pub struct Call {...} // from, to, gas, data, value

pub enum HaltReason {...} // out-of-gas, out-of-memory, ...
pub enum CallMode {...} // call, static, delegate, callcode, create, create2
pub enum Fetch {...} // code, nonce, balance, account, block, state

pub enum EvmYield {
    Return(Vec<u8>),
    Revert(Vec<u8>),
    Halt(HaltReason),
    Call(Call, CallMode),
    Fetch(Fetch),
}

pub type EvmResult<T> = std::result::Result<T, EvmYield>
    

YEVM: dispatch


pub fn dispatch<S: State>(
    op: u8,
    evm: &mut Evm,
    ctx: &Context,
    call: &Call,
    state: &mut S) -> EvmResult<()> {
    match op {
        0x00 => basic::stop(evm),
        // ...
        0x31 => chain::balance(evm, ctx, call, state),
        // ...
        0xFF => calls::selfdestruct(evm, ctx, call, state),
        _ => invalid(evm, ctx, call, state),
    }
}
    

YEVM: BALANCE handler


pub fn balance<S: State>(evm: &mut Evm, state: &mut S) -> EvmResult<()> {
    evm.gas_charge(100)?;
    let [acc] = evm.peek()?;
    let acc: Acc = acc.to();
    let Some(balance) = state.balance(&acc) else {
        return Err(EvmYield::Fetch(Fetch::Balance(acc)));
    };
    if state.warm_acc(&acc) {
        evm.gas_charge(2_500)?;
    }
    evm.push(balance)?;
    Ok(())
}
    

YEVM: pending state

Opcode handler:

  • stores pending state changes
  • applies them on success
  • reverts them on fetch

YEVM: streaming traces

  • Solenoid collected full per-opcode traces in memory → 55GB for Aztec rollup txs
  • YEVM emits trace events as a stream — consumer decides what to keep
  • Same observable design, zero memory blowup
  • Verified for 5M+ gas transactions with 100k+ steps

YEVM: streaming

  • Same code needs to work natively and in a browser
  • Has to be non-blocking and not use any sync primitives
  • 🤔

YEVM: streaming

  • Problem solved: futures::channel::mpsc
  • Needs explicit yields to reduce memory pressure
  • See sieve for more details

YEVM: streaming

YEVM: streaming

YEVM: results

  • 99%+ of Ethereum GeneralStateTests (Cancun fork) passing
    • not the case for Osaka fork (current one)
  • 1300+ mainnet blocks replayed — zero failed transactions
  • Comparable to revm performance on modern hardware
  • Seamlessly builds for wasm32-unknown-unknown target
    • minor code tweaks were still necessary

YEVM: benchmarks

3 random blocks from mainnet

  • B1: Block 24929490 (373 txs, 32 466 055 gas)
  • B2: Block 24929491 (74 txs, 20 653 399 gas)
  • B3: Block 25000000 (377 txs, 33 774 715 gas)

AVG time (ms) to execute full block:

Host B1 B2 B3
MacBook Pro M1774543
Intel NUC i7-1260P815150
Cloud VM (AMD EPYC)1308180

YEVM in the browser

Live demo: check vitalik.eth USDT balance

yevm

etherscan

YEVM in the browser

ByBit hack: Impl Swap TX (21895238:116)

YEVM in the browser

ByBit hack: Drain ETH TX (21895251:35)

Key takeaways


  • Async + Wasm is a valid architectural choice 🦀
  • The async tax is real and will bite if unmanaged 😵
  • Follow the spec (if you have one and it is good) 🙄
  • A/B testing against revm was totally worth it 💯


Learn more

Q&A

Thank you!