Stateless Fuzzing

A fuzz testing approach where each test run is independent, with no state carried between iterations.

Stateless fuzzing is a testing technique where a fuzzer generates random inputs for a function and executes each test in isolation, without maintaining state between runs. Each fuzz iteration starts fresh, making it excellent for testing individual functions but limited for discovering vulnerabilities that require specific sequences of operations.

How Stateless Fuzzing Works

In stateless fuzzing, the testing framework generates random values for function parameters and calls the function repeatedly with different inputs. Each call is independent—the contract state resets between iterations, and previous calls don't influence subsequent ones.

In Foundry, stateless fuzz tests are simple to write. Any test function that accepts parameters automatically becomes a fuzz test:

1function testFuzz_Withdraw(uint256 amount) public {
2 // Setup fresh state
3 payable(address(safe)).transfer(amount);
4
5 // Execute and verify
6 uint256 preBalance = address(this).balance;
7 safe.withdraw();
8 uint256 postBalance = address(this).balance;
9
10 assertEq(preBalance + amount, postBalance);
11}

Foundry's fuzzer will call this function hundreds of times (256 by default) with different values for amount, testing the withdraw logic across a wide range of inputs.

Stateless vs Stateful Fuzzing

The key distinction between stateless and stateful fuzzing lies in how state is handled between test iterations:

Stateless fuzzing resets contract state between each run. This is ideal for testing:

  • Individual function correctness
  • Input validation and boundary conditions
  • Arithmetic operations and edge cases
  • Pure and view functions

Stateful fuzzing (also called invariant testing) maintains state across calls, allowing the fuzzer to build up complex contract states through sequences of operations. This catches vulnerabilities that only emerge after specific state transitions.

Many critical vulnerabilities require stateful testing to discover. A reentrancy bug might only trigger after deposits and partial withdrawals create specific balance conditions. A governance attack might need multiple proposals and votes to materialize. Stateless fuzzing alone cannot find these issues.

Strengths of Stateless Fuzzing

Despite its limitations, stateless fuzzing offers several advantages:

Speed: Without state management overhead, stateless fuzz tests run extremely fast. Foundry can execute thousands of iterations per second, exploring vast input spaces quickly.

Simplicity: Stateless tests are easier to write and understand. Each test is self-contained, making failures straightforward to debug and reproduce.

Targeted testing: When you know a specific function needs thorough testing, stateless fuzzing efficiently explores its input space. This is particularly valuable for complex mathematical operations or input parsing logic.

Deterministic reproduction: Failed test cases are easy to reproduce because they don't depend on previous state. The failing input alone is sufficient to recreate the bug.

Writing Effective Stateless Fuzz Tests

To maximize the value of stateless fuzzing, follow these practices:

Bound your inputs: Use Foundry's bound() function to constrain random values to realistic ranges. Unbounded inputs often hit trivial failures (like out-of-gas) rather than meaningful bugs.

1function testFuzz_Deposit(uint256 amount) public {
2 amount = bound(amount, 1, 1000 ether);
3 // Now amount is always between 1 wei and 1000 ether
4}

Test boundary conditions explicitly: While fuzzing explores random values, explicitly test known boundaries like zero, maximum values, and threshold crossings.

Combine with unit tests: Use stateless fuzzing to complement, not replace, unit tests. Unit tests verify specific scenarios; fuzzing explores unexpected ones.

Increase run count for critical functions: Foundry's default 256 runs may not be enough for complex functions. Increase runs for critical code:

1[fuzz]
2runs = 10000

Limitations and When to Use Stateful Testing

Stateless fuzzing cannot discover vulnerabilities that require:

  • Specific sequences of function calls
  • Accumulated state from multiple operations
  • Interaction patterns between multiple users
  • Time-dependent state changes

For these scenarios, invariant testing is essential. A comprehensive security testing strategy uses both approaches: stateless fuzzing for function-level correctness and stateful fuzzing for protocol-level invariants.

Real-World Application

In security audits, stateless fuzzing often serves as a first pass. Auditors write fuzz tests for individual functions to quickly identify input handling bugs, arithmetic errors, and boundary condition failures. These findings then inform deeper stateful testing and manual review.

The combination of stateless fuzzing's speed with stateful testing's depth provides thorough coverage of both individual function behavior and complex protocol interactions.

Need expert guidance on Stateless Fuzzing?

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