Introduction
Fuzzing is essential for secure smart contract testing, but it comes with challenges. Test execution is often slow, and analyzing failures can take even more time and effort.
Shrinking addresses this problem. When fuzzing finds a bug during stateful testing, shrinking algorithms minimize the sequence of operations needed to reproduce it. This turns complex failures into simpler ones, making debugging faster and more efficient.
Modern fuzzers such as Foundry, Echidna, and Wake support shrinking, though each uses a different approach. This article compares their algorithms and the trade-offs behind each design.
Shrinking Algorithm in Foundry Invariant Tests
Source: Foundry shrink.rs
Foundry supports shrinking during invariant tests using a top-down approach. It tries to remove transactions from the start of the sequence and checks whether the error still occurs.
Process:
- Remove one transaction at a time, starting from the top.
- Re-run the sequence to check if the invariant failure persists:
- If the failure remains, keep the transaction removed.
- If it no longer fails, restore the transaction.
- Repeat until reaching the shrink limit or no further reduction is possible.
- Return the shortest sequence that still triggers the failure.
This method preserves the invariant failure but does not attempt to reproduce other bugs. It also does not simplify function call parameters.
Among the three tools, Foundry uses the simplest shrinking strategy.
Shrinking Algorithm in Echidna
Source: Echidna Shrink.hs
Echidna systematically shrinks failing test cases into minimal reproducible examples using a combination of structural reduction and parameter simplification.
- Replace reverting transactions with
NoCall
placeholders (except for the final transaction, which is always kept). - Remove unnecessary
NoCall
transactions that do not advance time or block number. - Apply one of two randomly chosen strategies:
- “Shorten”: Remove one randomly selected transaction.
- “Shrink”: Simplify all transactions by:
- Reducing argument values (e.g. to smaller numbers or simpler addresses)
- Lowering ETH amounts, gas prices, and time or block delays
- Replacing senders with simpler addresses
- Clean up any new useless
NoCall
transactions introduced by shrinking. - Re-run the sequence to confirm the failure is still reproducible.
- Repeat until reaching the shrink limit or no further reduction is possible.
Echidna supports both transaction and parameter shrinking, allowing it to produce highly reduced test cases for efficient debugging.
Shrinking Parameters
Echidna also simplifies function inputs by reducing values to smaller numbers and replacing addresses with simpler ones.
This ensures the bug remains reproducible while progressively simplifying the failing case, making debugging faster and easier for developers.
Shrinking Algorithm in Wake
Source: Wake fuzz_shrink.py
Phase 0: Collect Flow State
Re-run the fuzzing sequence to collect initial state data and detailed error context from the failing test.
Phase 1: Remove by flow kind
- List all flow function types and their call counts.
- Start with the most frequently called type and attempt to remove all calls of that kind.
- Re-run the sequence to check if the error still occurs.
Phase 2: Remove step by step
Go through the sequence from top to bottom, removing individual flow functions one at a time.
- If the error is still reproducible, keep the removal.
- If not, restore the removed call.
Use snapshots to skip re-execution of previously reduced parts of the sequence.
Wake also supports shortcuts during shrinking. If the same error is triggered earlier in the sequence, it replaces the original with this earlier instance, often leading to large reductions.
Key Differences Between the Three
The fuzzing strategies used in Foundry, Echidna, and Wake directly shape how each tool implements shrinking.
Foundry takes the simplest approach. It fuzzes by randomly calling functions and checking invariants. Its shrinking algorithm only removes transactions and does not simplify parameters.
Echidna runs for longer durations with shorter sequences, systematically exploring execution paths. This often leads to more compact failing cases by default. Its shrinking combines transaction removal with parameter simplification, using NoCall
placeholders and random strategies to minimize failing inputs.
Wake applies differential fuzzing by reimplementing contract logic in Python and verifying expected states. This allows for precise targeting of attack vectors and internal checks. However, Wake tends to generate longer sequences, which demand more aggressive shrinking. Its algorithm uses Python’s deepcopy
and EVM state snapshots to support flexible test composition. Unlike Echidna, it does not use a remove failing transaction for flexibility in the test.
Conclusion
The best fuzzing tool depends on your testing goals and workflow. Foundry prioritizes simplicity and speed. Echidna focuses on thorough minimization. Wake offers flexible, state-aware differential testing.
Choosing the right approach means balancing shrinking effectiveness, execution time, and debugging clarity. That balance is key to finding and fixing bugs efficiently.