# Wake Debugging Guide

> Master Wake's debugging tools: console logging, Python fuzzing, ipdb integration, and performance profiling for Solidity smart contract development.

- Canonical: https://ack3.ai/research/wake-debugging-guide/
- Published: January 14, 2026
- Authors: Naoki Yoshida
- Category: Technical guide
- Topics: education, ethereum, solidity, tutorial, wake

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:

```solidity
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:

```python
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](https://x.com/WakeFramework/status/1962863088144683511)

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.

```text
> 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:

```python
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:

```bash
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.

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

## More Tips

### [Handling Expected Reverts](../../wake/tuesday-x-articles/expecting-the-unexpected-revert-handling.md)

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:

```python
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](https://x.com/WakeFramework/status/1978811387511022053)

When tests run slowly, profile them to find bottlenecks:

```bash
wake --profile test tests/test_fuzz.py
```

This generates a profile file that you can visualize with:

```bash
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](https://x.com/WakeFramework/status/1975548855467843609)

For understanding test coverage, run:

```bash
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](https://x.com/WakeFramework/status/1967936361651900531)

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

```python
# 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](https://x.com/WakeFramework/status/1973735342579577320)

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

```python
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.
