About StackFlow
What is StackFlow?
StackFlow is an interactive control flow graph (CFG) explorer for EVM smart contracts. It bridges two tools: evm.codes, which lets you execute opcodes and watch the stack, memory, and storage state step by step, and bytegraph.xyz, which decompiles bytecode into a visual tree representing execution flow.
StackFlow combines both — executing transactions while visualizing transactions in real time, so you always know where you are in the execution flow. It also expands on evm.codes' functionality, giving users more control: caller address, deployment address, fork timestamp, and block number.
The Interface
Panels
- Editor — A code editor (powered by react-simple-code-editor). It accepts Solidity, Yul, Bytecode, and Opcodes.
- Execution State — The live EVM state snapshot after the most recent step: stack, memory, storage, transient storage, and the return value. All values are shown in hex.
- Console — Compilation output, deployment results, mapping status, and any runtime errors are streamed here.
How It All Fits Together
- Compile. If the input is Solidity or Yul, it is compiled via the solc-js compiler in a Web Worker. Raw bytecode or opcodes skip this step.
- Deploy. The bytecode is deployed to an in-browser EVM instance (a heavily customised fork of ethereumjs).
- Map. A separate EVM instance runs the runtime bytecode through a custom CFG mapping pass.
- Render. The mapping result is translated into React Flow nodes and edges, laid out by custom graph positioning, and rendered.
- Execute. Run the full transaction or step opcode-by-opcode through the same EVM. After each step the execution state panel updates with the new stack, memory, and storage.
Mapping the Control Flow Graph
The core challenge of building a CFG for arbitrary EVM bytecode is that jump destinations are computed at runtime. JUMP pops an address off the stack; JUMPI conditionally does the same. Without actually executing the code there is no reliable static way to know where a jump will land — so StackFlow does the next best thing: it executes the bytecode symbolically, tracking the stack state as it goes and following every branch it finds.
Depth-First Search
The mapping pass runs a depth-first search (DFS) over all reachable execution paths. Starting from program counter 0, opcodes are executed one by one, mutating a shared RunState object in place:
- Regular opcodes (arithmetic, stack manipulation, memory reads/writes, etc.) are executed normally via the full opcode handler. The program counter advances and the loop continues.
- Terminating opcodes (
STOP,RETURN,REVERT,INVALID,SELFDESTRUCT) end the path. JUMPpops the destination off the stack and updates the program counter directly.JUMPIis where branching actually happens. Both the true destination and the false (fall-through) destination need to be explored independently, each with their own copy of the current stack state.handleJumpIclonesRunStatefor each branch and callsexploreNodesrecursively for each — branching the DFS tree exactly where it needs to branch.
// Simplified structure
const opCode = runState.code[runState.programCounter]
switch (checkOpCode(opCode)) {
case OTHER: // Execution continues
case TERMINATOR: // Finalize Path
case JUMP: // Handle jump and continue
case JUMPI: // Branch recursion
}Stepping Through Execution
Once the graph is mapped, the contract is ready to actually run. The Run panel exposes the full EVM execution controls:
- Compile — compile the contract, Graph, and Tx data. Reset previous state (as needed)
- Step Forward — advance exactly one opcode and pause.
- Run — execute the full transaction in one go.
The result is a two-way view: the instruction list on the left shows the exact opcode being executed, and the graph on the right shows where that instruction sits in the overall logic of the contract. Conditional branches that were abstract edges in the graph become concrete — you can see in real time which branch was taken.
The Execution Environment
The EVM is a stack-based computer. Every instruction reads their parameters from the stack, except for PUSHx which load their operand directly from the bytecode. The stack holds 32-byte values and is capped at 1024 entries.
Beyond the stack there are three other data regions visible in the Execution State panel:
- Memory — a temporary byte array, initialised to zero at the start of each call and wiped when it ends. There is technically no limit, but size is bounded in practice by available gas.
- Storage — persistent 32-byte slot → 32-byte value mapping of the given contract. Changes here survive across transactions. Reading and writing storage is among the most expensive EVM operations.
- Transient Storage — introduced in EIP-1153 (Cancun), transient storage behaves like regular storage during execution but is wiped at the end of the transaction.
- Return data — the value returned by the most recent external call, or the output of the current execution if it has completed with
RETURN.
Technical Stack
A few of the libraries and tools that make StackFlow work:
- React Flow — graph canvas, node rendering, edge routing, and viewport controls.
- ethereumjs — the underlying EVM implementation, modified to drive the CFG mapping.
- solc-js — Solidity compiler compiled to WebAssembly, running in a Web Worker.
- Next.js + Tailwind CSS — app framework and styling.