3.2. Input Preprocessing
Fuzzing tools require the dynamic execution of smart contract transactions to thoroughly test the contract’s behavior. Therefore, preprocessing of the smart contract source code is essential. This preprocessing step involves extracting several critical components, including the EVM bytecode for contract deployment, the interface information for contract invocation, and the CFG and data dependency information for generating auxiliary test cases. The following outline each of these processes in detail.
(A) Compilation: The first step in preprocessing is compiling the smart contract source code using the
solc compiler. This process generates a JSON artifact that includes several key elements: the ABI (Application Binary Interface), the AST (Abstract Syntax Tree), and the EVM bytecode. These components play distinct roles in enabling effective contract execution and vulnerability analysis. The specific explanations and roles of these compilation results are summarized in
Table 3.
(B) CFG Construction: The next critical preprocessing step is the construction of the CFG, which is formally defined as a directed graph
, comprising basic blocks as vertices
V and control transitions as edges
E. Each basic block encapsulates a continuous sequence of non-branching EVM instructions, demarcated by start/end addresses. As detailed in Algorithm 1, CFG construction is implemented through bytecode stream analysis:
JUMPDEST opcodes and
JUMP/
JUMPI instructions partition basic blocks, with static jump targets derived from preceding
PUSH operands. Conditional jumps (
JUMPI) generate dual edges for both branches. Note that for some dynamic jump edges, their determination must occur during subsequent symbolic execution.
Algorithm 1 CFG construction |
Input: EVM runtime bytecode R Output: Control flow graph - 1:
Initialize , - 2:
, - 3:
for to do - 4:
- 5:
if then - 6:
- 7:
end if - 8:
if then - 9:
- 10:
- 11:
- 12:
end if - 13:
if then - 14:
- 15:
- 16:
- 17:
{JUMP: , JUMPI: } - 18:
else if then - 19:
- 20:
- 21:
{Skip operand bytes} - 22:
else - 23:
- 24:
end if - 25:
- 26:
end for - 27:
return
|
(C) Data Dependency Analysis: In addition to CFG construction, another crucial analysis involves data dependency, which plays a key role in detecting vulnerabilities like deep-N vulnerability [
50], such as cross-function reentrancy attacks. These vulnerabilities are often triggered by a specific sequence of function calls that modify global state variables. To address this, the proposed method constructs a vulnerability trigger model through data dependency analysis, examining the read and write operations of functions on global state variables. Algorithm 2 implements function-level data flow analysis, extracting the set of global variables from the AST, traversing the function’s statement structure to identify state variable writes in assignment operations (recorded in
) and variable references within expressions (recorded in
). For cross-function calls, the target is resolved through ABI function signature matching, and the read/write dependencies of the called function are inherited through the union operation on the sets.
Algorithm 2 Data dependency analysis |
Input: Compiled contract result jsonContent Output: , (function → variable set mappings)
- 1:
- 2:
- 3:
- 4:
Initialize - 5:
for each do - 6:
- 7:
- 8:
for each do - 9:
if is Assignment then - 10:
- 11:
if then - 12:
- 13:
- 14:
end if - 15:
else if is VariableReference then - 16:
if then - 17:
- 18:
end if - 19:
else if is FunctionCall then - 20:
- 21:
if then - 22:
- 23:
- 24:
end if - 25:
end if - 26:
end for - 27:
end for - 28:
return ,
|
3.3. Two-Phase Hybrid Fuzzing Engine
The Two-Phase Hybrid Fuzzing Engine is the core module of our framework, designed to efficiently detect vulnerabilities in smart contracts by combining coverage-guided and vulnerability-guided fuzzing strategies. This engine operates in two distinct yet complementary phases, ensuring both breadth (exploring all possible paths) and depth (focusing on high-risk areas) in vulnerability detection.
Phase ①: Coverage-Guided Fuzzing: In this phase, the engine generates a wide variety of test cases to explore untested execution paths. For instance, when a smart contract includes a function with multiple conditional branches, this phase attempts to traverse each branch, by varying input parameters through a combination of symbolic analysis and random mutation strategies. The symbolic analysis method negates branch conditions and solves for uncovered path constraints, while the random mutation approach introduces new inputs when constraints cannot be satisfied symbolically. This phase initializes a special individual pool, which records those test cases that reach potential vulnerability areas.
Phase ②: Vulnerability-Guided Fuzzing: Building on the outcomes of the first phase, this phase focuses on targeted fuzzing using the special individual pool. Here, the transaction sequences of the stored test cases are fixed, while the fuzzer exclusively optimizes the parameters of these transactions. For example, if during the coverage-guided phase certain inputs cause unexpected state changes in a contract function handling fund transfers, the vulnerability-guided phase will prioritize these cases, refining the transaction parameters to intensify testing in that specific area.
In both phases, the Symbolic Analyzer and Sequence Optimizer play crucial roles in analyzing the results of test case execution. They provide valuable insights, which are then fed back into the fuzzing engine to guide subsequent testing.
(A) Symbolic Analyzer: One key component of our fuzzing engine is the Symbolic Analyzer, as outlined in Algorithm 3. Unlike traditional symbolic execution—which must explore every possible execution path, leading to exponential path explosion as contract complexity increases—our approach leverages dynamic symbolic execution to focus solely on the paths that are actually executed during fuzzing. This strategy significantly mitigates the path explosion problem while concentrating the analysis on the most relevant execution branches. The analyzer extracts path constraints (e.g., transaction parameters satisfying
require(msg.value > 0)) and branch conditions from the traces. For each conditional branch, the analyzer constructs a constraint system. It combines historical path conditions with the negation of the target branch condition. Using constraint solvers, the analyzer generates new input values that satisfy these conditions. These solved values are then injected into the original test case structure. The fields, such as function selector and address formats, are preserved. This creates mutated seeds that are fed back to the fuzzer. This process enables targeted exploration of complex branching logic. Random mutation often cannot efficiently reach these paths.
Algorithm 3 Symbolic analyzer |
Input: Execution trace execution_trace Output: List of new mutation seeds new_seeds- 1:
Initialize new_seeds - 2:
for each branch in extract_branches(execution_trace) do - 3:
path_constraints ← branch.get_path_constraints() - 4:
negated_condition ← negate_condition(branch.condition) - 5:
new_constraints ← path_constraints + [negated_condition] - 6:
solved_input ← solve_constraints(new_constraints) - 7:
if solved_input is valid then - 8:
mutated_seed ← mutate_seed(branch.input_context, solved_input) - 9:
new_seeds.append(mutated_seed) - 10:
end if - 11:
end for - 12:
return
new_seeds
|
(B) Sequence Optimizer: Equally important is the Sequence Optimizer, which contributes significantly to exploring the deep state space of the contract. Algorithm 4 outlines its process, which leverages data dependency relationships obtained during preprocessing (for simple variable types) and dynamic data dependency relationships observed during runtime (for complex types such as mappings or arrays) to generate input sequences. When exploring regions that may potentially contain vulnerabilities, the Sequence Optimizer performs reverse analysis on the CFG to derive complete call sequences. These sequences are then used to guide the generation of test cases that target specific vulnerabilities.
(C) EVM Sandbox: Additionally, the fuzzing engine incorporates the EVM Sandbox, which serves a crucial role in ensuring secure and controlled testing. This sandbox is a secure and isolated environment built on Ethereum’s official Python implementation (Py-EVM version 0.9.0). It faithfully simulates the EVM, and by leveraging Py-EVM’s execution semantics, the sandbox removes real-world blockchain operations like block mining and transaction encoding/decoding. This allows it to focus solely on the precise execution of transactions. While this isolation ensures safety and focused testing, it also means that the real status or behavior of external contracts cannot be directly obtained. To address this, the test framework sets predefined return values or statuses for external calls within the test cases, thus avoiding potential failures caused by the absence of real external contracts during testing. External calls are simulated by adding hook functions to the test framework, which replicate the expected results of these calls. During testing, the sandbox records detailed execution traces, including opcode-level execution flow,
gas consumption, and storage state changes.
Algorithm 4 Sequence optimizer |
Input: Control Flow Graph , Data Dependencies , Output: Optimized test sequence set - 1:
Initialize - 2:
critical node set - 3:
for
do - 4:
- 5:
for do - 6:
- 7:
if then - 8:
- 9:
- 10:
end if - 11:
end for - 12:
end for - 13:
return
|
3.4. Vulnerability Detector
The vulnerability detection module operates through a multistage analytical process that begins with the execution traces generated during the fuzzing campaign. These traces, captured from the EVM during test case execution, comprehensively record the contract’s runtime behavior, including function call sequences, stack operations, memory modifications, and storage changes. By analyzing these temporal records of bytecode-level operations, the system reconstructs the complete execution context necessary for vulnerability verification, as raw transaction logs alone cannot reveal subtle data flow anomalies.
To precisely model contract behavior, the module implements a full EVM simulator that dynamically interprets each opcode while maintaining virtualized representations of stack frames, memory buffers, and persistent storage slots. This simulation process goes beyond literal execution by augmenting concrete values with symbolic metadata—for instance, tracking not just the numerical result of an ADD operation but also its semantic relationship to prior computations. During this emulation, the system injects taint markers into security-sensitive inputs identified through predefined sources like function parameters (
CALLER,
CALLVALUE), block state variables (
TIMESTAMP,
NUMBER), and external call returns, as systematically categorized in
Table 4. These taint labels then propagate through subsequent operations following EVM-specific rules: arithmetic instructions (
MUL,
SUB) inherit taint status from their operands, control flow operations (
JUMPI) carry taint conditions to destination addresses, and storage operations (
SSTORE) preserve taint states across transactions. This taint tracking mechanism crucially distinguishes between benign data flows and attacker-controllable paths, reducing false positives in vulnerability detection.
The actual vulnerability identification matches model patterns in synchronization with execution simulation. Our framework categorizes vulnerabilities into two groups based on the detection strategy:
Require Specific Transaction Sequences: For vulnerabilities such as RE, AF, UD, and US, our system generates specialized test cases designed to reach targeted code segments. For example, to detect RE, a specific function sequence (e.g., deposit–withdraw–fallback) is generated. During execution simulation, the monitor tracks external call patterns (such as CALL and DELEGATECALL) occurring between contract state updates, flagging instances where tainted parameters control call targets or value transfers. Similarly, for UD, the system constructs transaction sequences that reach delegate call statements and then verifies whether the call parameters have been influenced by tainted inputs, as determined by our vulnerability pattern analysis.
Direct Execution Trajectory Analysis: Other vulnerabilities—such as IO, UC, BD, and EF—are identified directly by analyzing the execution trajectory. For instance, when detecting IO, the system compares arithmetic operation results against the EVM’s 256-bit modular arithmetic boundaries, while simultaneously verifying whether tainted inputs have influenced the computation. An alert is triggered only when the suspect value flows into security-critical contexts, such as fund transfer amounts or array indices. For UC, the system examines whether block information variables have contaminated key instructions like STATICCALL, SELFDESTRUCT, or DELEGATECALL.
Each detected vulnerability event is documented with its triggering transaction sequence, affected state variables, a visualization of the taint propagation path, and a severity assessment that includes the specific source code location. This comprehensive approach ensures that both tailored test case generation and direct execution analysis work in tandem to accurately detect and categorize vulnerabilities in practice.
While the vulnerability detection module effectively uses taint analysis and symbolic execution, it faces trade-offs between false positives and false negatives. False positives may occur when detected vulnerabilities are actually intentional design choices by the contract developer, such as specific reentrancy patterns, which are not real security risks. These cannot be fully eliminated, but efforts are made to reduce them. False negatives arise from three factors, incomplete vulnerability definitions, inability to simulate external interactions, and insufficient branch coverage, all of which can lead to missed vulnerabilities. Our tool design addresses these challenges through detailed vulnerability pattern analysis, simulating external interactions, and employing two-phase fuzzing strategies to improve branch coverage.