When a fuzz run fails or contract behavior surprises you, effective debugging turns hours of frustration into minutes of focused investigation. Wake gives you Python’s full debugging ecosystem combined with Solidity-specific visibility tools. This guide shows you the techniques Ackee’s audit team uses to isolate issues fast.

Visibility Into Contract State

The first step in debugging any failing test is understanding what your contracts are actually doing. Wake provides several approaches to expose internal state.

Console Logging in Solidity

Wake includes a console logging library that works directly in your Solidity code:

import "wake/console.sol";

You can log values inline during contract execution with console.log(), console.logBytes32(), and console.logBytes(). The library includes variants for dynamic-length data. This gives you printf-style debugging without deploying to a live network or setting up complex tooling.

When you need readable call traces, use account.label to replace raw addresses with meaningful names. Instead of seeing 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb in traces, you’ll see “TokenContract” or “UserWallet.”

For pure functions, you’ll need a workaround since they can’t emit logs during normal execution. The quick fix is temporarily marking the function and any dependent pure functions as view. If that’s not feasible, emit a custom event instead.

Manually Guided Fuzzing (MGF) Techniques

Wake’s Python-based fuzzing gives you control that coverage-guided approaches can’t match. The key is maintaining accurate Python state that mirrors your contract state.

Invariant Functions

Write more invariant functions than flow functions. Invariants verify your Python state stays synchronized with on-chain reality, which becomes essential when debugging complex failures. When a test fails, you can trust your Python state to understand what went wrong.

Find the Flow That Caused a Failure

To identify which flow caused a failure, add logging at the start of each flow:

def pre_flow(self, flow: Callable):
    logger.info(f"[FLOW] {flow.__name__}")

This creates a breadcrumb trail showing exactly which sequence of operations led to the failure.

Interactive Debugging with ipdb

Wake integrates seamlessly with Python’s debugger. When you need to inspect state mid-execution or step through transaction sequences, drop into ipdb.

Transaction Debugging

To examine recent transactions, use chain.txs[-1].call_trace for the most recent transaction or chain.txs[-2].call_trace for the one before. This gives you complete visibility into what happened on-chain.

> chain.txs[-1].call_trace
> chain.txs[-2].call_trace

Systematic Debugging Setup

For systematic debugging, set up handlers that automatically print traces when problems occur:

def revert_handler(e: RevertError):
    if e.tx is not None:
        print(e.tx.call_trace)

def tx_callback_fn(tx: TransactionAbc) -> None:
    print(tx.call_trace)
    # so no print(tx.call_trace) everywhere, only for debug because too slow

@chain.connect()
@on_revert(revert_handler)
def test_fuzz_stethpool():
    chain.tx_callback = tx_callback_fn
    FuzzVWStEth().run(1, 100000)

Note that printing traces for every transaction slows execution significantly, so enable tx_callback only when actively debugging.

Reproducing with Random Seeds

When you’ve found a failure, reproduce it with the exact random seed:

wake test tests/test_counter_fuzz.py -S62061e838798ad0f -d -v

The -d flag drops you into the debugger, -v increases verbosity, and the seed ensures you hit the same sequence every time.

Shrinking Failed Sequences

Once you’ve found a failing sequence, shrinking isolates the minimal flow combination that triggers the bug. This confirms whether the issue appears earlier in the sequence or requires the full setup.

Run shrinking with wake test tests/test_something.py -SH to shrink to a human-readable sequence or -SR for a compact representation. The smaller sequence often reveals the core issue more clearly than the full failure.

wake test tests/test_something.py -SH
wake test tests/test_something.py -SR

More Tips

Handling Expected Reverts

When testing edge cases, you’ll often want to verify that operations revert under specific conditions. Wake’s may_revert context manager lets you assert both successful execution and expected failures:

with may_revert() as e:
    tx = contract.operation()

if condition_that_causes_revert:
    assert e.value == Contract.ExpectedError()
    return "expected_revert_reason"

assert e.value is None  # Must succeed otherwise

This pattern makes tests self-documenting – it’s immediately clear which paths should revert and which should succeed.

Performance Analysis

When tests run slowly, profile them to find bottlenecks:

wake --profile test tests/test_fuzz.py

This generates a profile file that you can visualize with:

gprof2dot -f pstats .wake/wake.prof | dot -Tsvg -o wake.prof.svg

The resulting SVG shows exactly where execution time goes, making optimization targets obvious.

Fuzzing Coverage

For understanding test coverage, run:

wake test tests/test_fuzz.py --coverage

This generates coverage.cov in your project root. Open VS Code’s command palette and select “Wake: Show Coverage” to visualize which contract code your tests exercised.

Token Testing Patterns

Testing DeFi protocols requires minting tokens and managing approvals. Wake makes this straightforward:

# Mint directly to your test user
mint_erc20(token, user, 100 * 10**18)
token.approve(contract, 100 * 10**18, from_=user)
contract.pullToken(from_=user)

Cross-Chain Testing

For cross-chain scenarios, Wake lets you run multiple independent chains simultaneously:

from wake.testing import Chain

# Create two separate blockchain instances
ethereum_chain = Chain()
polygon_chain = Chain()

@ethereum_chain.connect()
@polygon_chain.connect()
def test_cross_chain_transfer():
    # Both chains are now connected and ready
    pass

Making Debugging Faster

Start with clear invariants that verify your assumptions about contract state. Keep call traces easily accessible through handlers and callbacks. When tests fail, shrink them to minimal reproducers before diving deep. These patterns make the difference between spending hours hunting bugs and isolating them in minutes.

The techniques here leverage Python’s mature debugging ecosystem while giving you direct access to EVM internals. Wake combines the expressiveness of Python with the precision needed for smart contract security. That’s the foundation for both fast development and thorough testing.