Symbolic Execution

A program analysis technique that uses symbolic values instead of concrete inputs to explore all possible execution paths simultaneously.

Symbolic execution is a powerful program analysis technique used in formal verification to mathematically prove properties about code. Instead of running a program with specific concrete values (like testing with input=5), symbolic execution uses abstract symbolic values (like input=α) that represent all possible inputs simultaneously. This allows security tools to explore every possible execution path in a single analysis, catching vulnerabilities that traditional testing might miss.

How Symbolic Execution Works

Consider a simple function:

1function withdraw(uint256 amount) external {
2 require(amount <= balance[msg.sender], "Insufficient balance");
3 balance[msg.sender] -= amount;
4 (bool success,) = msg.sender.call{value: amount}("");
5 require(success, "Transfer failed");
6}

With concrete testing, you might test amount = 100 and verify it works. With symbolic execution:

  1. Assign symbolic values: amount = α, balance[msg.sender] = β
  2. Explore all paths:
    • Path 1: α > β → reverts with "Insufficient balance"
    • Path 2: α ≤ β → continues to transfer
  3. Build constraints: For each path, accumulate conditions that must be true
  4. Solve constraints: Use SMT solvers to find concrete values that reach each path

The result is mathematical certainty about what conditions lead to what outcomes.

Symbolic Execution vs Traditional Testing

AspectConcrete TestingSymbolic Execution
Input valuesSpecific (e.g., 100)Abstract (e.g., α)
Path coverageOne path per testAll paths simultaneously
CompletenessTests what you writeExplores exhaustively
Finding bugsNeeds lucky inputsMathematically guaranteed
SpeedFast per testSlower, but comprehensive

Traditional testing asks "does this specific input cause a bug?" Symbolic execution asks "does any possible input cause a bug?"

SMT Solvers: The Engine Behind Symbolic Execution

Satisfiability Modulo Theories (SMT) solvers are the mathematical engines that make symbolic execution practical. When symbolic execution builds path constraints like:

1α ≤ β ∧ β < 1000 ∧ α > 500

The SMT solver determines:

  1. Is this constraint satisfiable? (Can all conditions be true simultaneously?)
  2. If yes, provide a concrete example (e.g., α=600, β=700)

Popular SMT solvers include Z3 (Microsoft), CVC5, and Yices. Tools like Halmos use these solvers to verify smart contract properties.

Path Explosion Problem

The main challenge with symbolic execution is path explosion. Every conditional branch doubles the number of paths to explore:

1if (condition1) { ... } // 2 paths
2if (condition2) { ... } // 4 paths
3if (condition3) { ... } // 8 paths
4// After 30 conditions: over 1 billion paths

Loops make this worse—a loop running n times creates n unrollings to analyze. Smart contract verification tools handle this through:

  • Loop bounding: Analyze loops up to a fixed number of iterations
  • Abstraction: Summarize complex operations
  • Incremental solving: Reuse solver results across similar paths

Symbolic Execution in Smart Contract Security

Several tools use symbolic execution for Solidity verification:

Halmos: Symbolic testing framework that runs Foundry-style tests with symbolic inputs instead of fuzzed values. Proves properties hold for all possible inputs.

Mythril: Security analysis tool combining symbolic execution with other techniques to detect vulnerabilities automatically.

HEVM: Symbolic EVM implementation used for equivalence checking and property verification.

Manticore: Dynamic symbolic execution tool for EVM bytecode analysis.

Practical Example: Finding a Bug

Consider this vulnerable function:

1function divideReward(uint256 totalReward, uint256 numRecipients)
2 external pure returns (uint256)
3{
4 return totalReward / numRecipients; // Division by zero possible!
5}

Symbolic execution with constraints:

  • totalReward = α (any uint256)
  • numRecipients = β (any uint256)

The tool explores paths and finds:

  • Path where β = 0 causes revert (division by zero)
  • SMT solver confirms: constraint β = 0 is satisfiable

Result: Bug found mathematically, with concrete counterexample numRecipients = 0.

Combining with Fuzzing

Symbolic execution and fuzzing complement each other:

FuzzingSymbolic Execution
Fast, broad explorationDeep, complete analysis
Good for finding crashesGood for proving absence of bugs
May miss rare pathsExplores all paths
Works on complex codeStruggles with path explosion

Modern security approaches use both: fuzzing for rapid bug discovery, symbolic execution for mathematical guarantees on critical properties.

Limitations

State explosion: Complex contracts with many storage variables create enormous state spaces.

External calls: Interactions with other contracts are difficult to model symbolically.

Concrete operations: Some operations (cryptographic hashes, external data) can't be reasoned about symbolically and must be approximated.

Computational cost: Thorough symbolic analysis can take hours or days for complex contracts.

When to Use Symbolic Execution

Symbolic execution is most valuable for:

  1. Critical invariants: Prove that key properties (like "total supply equals sum of balances") always hold
  2. Access control: Verify that only authorized addresses can call sensitive functions
  3. Arithmetic correctness: Prove absence of overflow, underflow, or division by zero
  4. Equivalence checking: Verify that an optimized function behaves identically to a reference implementation

For smart contract security, symbolic execution provides the mathematical certainty that testing alone cannot achieve.

Need expert guidance on Symbolic Execution?

Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.

Get a Quote

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx