Fuzzing Smart Contracts in the Browser: The Architecture Behind WASM-Based Security Testing
Coverage-guided fuzzing is among the most effective techniques for finding smart contract vulnerabilities. It’s also, traditionally, a backend operation — a long-running server process that generates thousands of inputs and uses coverage feedback to steer toward unexplored paths.
This post covers a browser-native alternative: a fuzzer compiled to WebAssembly that runs entirely client-side. The motivation, the browser constraints that required engineering work, and the tradeoffs against server-side fuzzing.
Why Run Fuzzing in the Browser?
Server-side fuzzing is faster and scales better. The browser is sandboxed, single-threaded, and has no persistent filesystem. The case for building one anyway comes down to trust boundaries and deployment friction.
Security-sensitive users — auditors, protocol developers, DAO security committees — sometimes cannot send bytecode to a third-party server. The value being protected is often hundreds of millions of dollars. Browser-based execution removes that trust requirement: the WASM module downloads once and runs locally. Bytecode never leaves the machine.
The second motivation is installation friction. No Docker container, no Rust toolchain, no dependency resolution. Any machine with a browser works.
The WASM Compilation Architecture
The fuzzer is written in Rust and compiled to WebAssembly using wasm-pack, which handles the Rust-to-WASM compilation and generates JavaScript bindings. The browser receives a .wasm binary and a JavaScript glue layer.
flowchart TD
classDef process fill:#1a2233,stroke:#7ea8d4,stroke-width:2px,color:#c0d8f0
classDef data fill:#332a1a,stroke:#d4b870,stroke-width:2px,color:#f0e0c0
classDef system fill:#1a3333,stroke:#5ba8a8,stroke-width:2px,color:#c0e8e8
classDef highlight fill:#332519,stroke:#e8a87c,stroke-width:2px,color:#f0d8c0
subgraph Build ["Build Pipeline"]
R["Rust Fuzzer Source"]:::process
W["wasm-pack compile"]:::process
B["WASM binary + JS glue"]:::data
end
subgraph Browser ["Browser Runtime"]
JS["JavaScript host"]:::system
WM["WASM Module"]:::process
EVM["EVM Interpreter (WASM)"]:::process
FC["Fuzzing Core (WASM)"]:::process
CO["Coverage Oracle (WASM)"]:::process
end
R --> W --> B
B --> WM
WM --> EVM
WM --> FC
WM --> CO
JS --> FC
FC --> JS
Four components live inside the WASM module:
EVM interpreter. A complete EVM execution engine compiled to WASM — the full instruction set, memory model, storage, and call stack. Same interpreter used for backend symbolic execution.
Fuzzing core. The mutation engine and corpus manager. Maintains interesting inputs and applies mutation operators to generate new candidates.
Coverage oracle. Records which basic blocks were reached after each execution. Steers mutation toward inputs that discover new coverage.
JavaScript host. Glue layer that accepts bytecode and function signatures from the UI and passes findings back to the React components.
Coverage-Guided Mutation
Coverage-guided fuzzing treats code coverage as a fitness signal. Inputs that reach new basic blocks join the corpus; inputs with redundant coverage are discarded. Over thousands of iterations, the corpus grows to cover more of the contract’s behavior.
The mutation operators:
Calldata mutation. The fuzzer understands ABI encoding: 4-byte function selectors, 32-byte address slots with zero-padding, and boundary values for uint256 fields (0, 1, type(uint256).max, type(uint128).max).
Sequence mutation. Contracts are stateful. A single-call fuzzer misses vulnerabilities requiring specific call sequences. The fuzzer mutates at the sequence level: inserting calls, reordering them, or swapping individual arguments.
Value injection. For payable functions, ETH value is a fuzzing dimension. The fuzzer tries zero, small values, and very large values at all payable call sites.
Known-interesting values. Seeds derived from static analysis. If the decompiler found a contract checking msg.sender == 0xDeadBeef..., that address becomes a corpus seed. Symbolic execution pre-seeds values known to reach otherwise-unreachable branches.
Browser Constraints
Single-Threaded Execution
The main WASM thread runs synchronously in the JavaScript event loop. A fuzzing loop that runs for 30 seconds locks the UI completely. The solution: run the WASM module in a Web Worker and communicate via message passing:
Worker thread (WASM fuzzer):
loop:
mutate input from corpus
execute in EVM interpreter
collect coverage
if new coverage: add to corpus
if crash/property violation: post finding to main thread
every 100ms: post progress update
Main thread (React UI):
on worker message:
update coverage chart
display new findings
continue rendering
The fuzzer keeps running; the UI stays responsive.
No Persistent Filesystem
The fuzzer maintains corpus state in memory. Users can export the corpus as JSON at session end and re-import it later. Crash inputs surface immediately in the UI with the full triggering calldata sequence.
Memory Limits
Browser tabs have practical memory ceilings. The fuzzer runs corpus minimization when the corpus reaches a configurable size threshold, removing inputs whose coverage is fully subsumed by other members.
No Network Access from WASM
The WASM module cannot make network requests. For analysis requiring on-chain state, the JavaScript host fetches data from the RPC layer and passes it to the WASM module during setup. Storage slots are pre-populated with current values before the fuzzing session begins.
What the Browser Fuzzer Finds Well
Invariant violations. Given a contract and invariants (e.g., totalSupply == sum(balances) for tokens, totalBorrows <= totalCollateral for lending), the fuzzer searches for inputs that violate them. Violations represent arithmetic bugs, accounting errors, or logic flaws.
Arithmetic edge cases. Boundary value injection — max uint256, zero, one, values near 2^128 — reliably triggers overflow/underflow in pre-0.8 contracts and edge cases in custom fixed-point arithmetic.
Access control misconfigurations. The fuzzer explores every function with every caller address in its corpus. Functions that execute sensitive operations from non-owner addresses produce findings automatically.
Reverting conditions. The fuzzer maps the space of inputs that revert versus succeed for each function — useful for understanding behavior before writing a formal specification.
Where Server-Side Fuzzing Remains Better
Throughput. Server-side fuzzers execute millions of inputs per second. The WASM fuzzer achieves thousands, limited by single-threaded execution and browser overhead.
Time budget. Browser sessions end when the tab closes. Server-side fuzzing runs for hours or days, finding bugs in obscure paths requiring rare state combinations.
Multi-contract analysis. Cross-contract attack chains require simulating interactions between multiple deployed contracts. Server-side orchestration queries the chain directly; browser setup is more constrained.
Corpus persistence. Manual export/import works but doesn’t replace automated corpus management of a persistent server-side fuzzer.
Performance Profile
On a modern laptop (M2 MacBook Pro, Safari):
| Contract Size | Functions | Exec/sec | Typical time to first finding |
|---|---|---|---|
| Small (~50 opcodes) | 3-5 | ~8,000 | < 10 seconds |
| Medium (~500 opcodes) | 10-20 | ~2,500 | 30-120 seconds |
| Large (~2,000 opcodes) | 30+ | ~800 | 2-10 minutes |
| Complex DeFi (~10,000 opcodes) | 50+ | ~200 | 10-30 minutes |
Complex DeFi protocols are at the edge of what browser-based fuzzing handles in a reasonable session. Scoping to specific functions of interest works better than analyzing the full contract surface.
EVM vs. SVM: Separate WASM Modules
Two fuzzer modules ship: one for EVM, one for Solana BPF programs. They share the fuzzing core (mutation, corpus management, coverage) but differ in the interpreter. The SVM fuzzer interprets BPF bytecode — a RISC-style 64-bit register instruction set, smaller and faster than the EVM interpreter. The fuzzing strategy also differs: Solana programs receive account arrays with ownership and signer semantics, so the fuzzer generates valid account structures alongside instruction data. Finding classes diverge accordingly: missing signer checks, incorrect owner validation, PDA seed collisions, and arbitrary CPI targets.
For contracts where a 10-minute browser session covers the interesting behavior space, WASM fuzzing delivers findings with no backend dependency and no bytecode leaving the machine. The trust model alone justifies the approach; the accessibility is a bonus.