# Mastering Wake Printers for Solidity Security Analysis

> Learn how to build custom Wake printers for smart contract analysis, uncover vulnerabilities, and speed up Solidity security reviews.

- Canonical: https://ack3.ai/research/mastering-wake-printers-for-solidity-security-analysis/
- Published: November 6, 2025
- Authors: Naoki Yoshida
- Category: Technical guide
- Topics: education, ethereum, hack, printer, security, smart-contract, solidity, tutorial, wake

## Introduction

Manually reviewing Solidity code is slow and prone to errors. A single overlooked function can hide a costly vulnerability. Wake’s printer system automates the search for these risky patterns, turning hours of manual review into quick, reliable scans.

Wake printers combine Python’s simplicity with Wake’s Intermediate Representation (IR) to turn complex static analysis into simple scripts that highlight things like unrestricted withdrawals or missing access controls. This guide will walk you through creating custom printers that highlight security-relevant patterns in your smart contracts.

## Prerequisites and Setup

For this tutorial, we’ll use the workshop repository as our example project:

```bash
git clone https://github.com/Ackee-Blockchain/2025-workshop-fuzzing
cd 2025-workshop-fuzzing
npm install
```

Before continuing, check that Wake compiles your project successfully by running:

```bash
wake up
```

## Understanding Wake Printers

Wake comes with several built-in printers that showcase different kinds of analysis. You can list them with:

```bash
wake print
```

Run a specific printer by name:

```bash
wake print storage-layout
```

![](/research/media/2025/11/Screenshot-2025-11-04-at-18.32.53.png)storage layout output

Built-in printers demonstrate the system’s capabilities, but the real power comes from writing custom printers tailored to your security analysis needs. Once you understand how built-in printers work, you’ll see how easy it is to extend Wake with your own analysis tools. By the end of this guide, you’ll know how to create printers that detect vulnerability patterns relevant to your auditing approach.

## Tutorial 1: Creating Your First Printer – Listing Contracts

Let’s start with a simple printer that lists all contracts in your project. This example introduces the core concepts you’ll use in more complex analysis.

### Creating the Printer Structure

Run this command to scaffold your first printer::

```bash
wake up printer list-contracts
```

![](/research/media/2025/11/Screenshot-2025-11-04-at-17.40.37.png)

Wake generates a new printer directory and a starter file with this structure:

  * `printers/` directory for all custom printers
  * `list-contracts.py` with the basic printer structure



### Understanding the Template

The generated template provides this starting structure:

```python
from __future__ import annotations

import networkx as nx
import rich_click as click
import wake.ir as ir
import wake.ir.types as types
from rich import print
from wake.cli import SolidityName
from wake.printers import Printer, printer


class ListContractsPrinter(Printer):
    def print(self) -> None:
        pass

    @printer.command(name="list-contracts")
    def cli(self) -> None:
        pass
```

Here’s what each part of the template does:

  * **`print()`** : Main execution method where analysis results are displayed
  * **`cli()`** : Command-line interface handler for custom arguments



### Implementing the Visitor Pattern

Wake uses the Visitor pattern to traverse the contract’s Abstract Syntax Tree (AST). The visitor pattern allows Wake to automatically navigate through your code’s structure, enabling you to react to specific elements—such as contract or function definitions.

To list contracts, we’ll override the `visit_contract_definition` method, which gets called for each contract in the codebase.

Add this method to your `ListContractsPrinter` class:

```python
def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
    print(node.name)
```

Test your printer:

```bash
wake print list-contracts
```

This command runs your printer and prints all contract names found in your project.

### Refining the Output

The basic implementation shows all contracts, including interfaces and inherited contracts. Let’s improve it to show only deployable contracts:

```python
from __future__ import annotations

import networkx as nx
import rich_click as click
import wake.ir as ir
import wake.ir.types as types
from rich import print
from wake.cli import SolidityName
from wake.printers import Printer, printer


class ListContractsPrinter(Printer):

    def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
        print(node.name)

    def print(self) -> None:
        pass

    @printer.command(name="list-contracts")
    def cli(self) -> None:
        pass
```

Right now, the printer lists every contract, including interfaces and base classes. Let’s refine it to show only deployable ones.

![](/research/media/2025/11/Screenshot-2025-11-04-at-18.32.38.png)

### Filtering for Deployable Contracts

Add conditions to filter out interfaces, libraries, and base contracts. This helps identify which contracts are actually deployable:

```python
def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
    if len(node.child_contracts) != 0:
        return

    if node.kind != ir.enums.ContractKind.CONTRACT:
        return

    print(node.name)
```

The ContractDefinition class includes attributes you can use to filter your results. For complete reference, see: </wake/docs/latest/api-reference/ir/declarations/contract-definition/>

### Complete Implementation

Here’s the final version with proper separation of concerns—collecting data during traversal and displaying it in the `print()` method:

```python
from __future__ import annotations

import networkx as nx
import rich_click as click
import wake.ir as ir
import wake.ir.types as types
from rich import print
from wake.cli import SolidityName
from wake.printers import Printer, printer


class ListContractsPrinter(Printer):
    contracts: list[ir.ContractDefinition]

    def __init__(self):
        self.contracts = []

    def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
        if len(node.child_contracts) != 0:
            return

        if node.kind != ir.enums.ContractKind.CONTRACT:
            return

        self.contracts.append(node)

    def print(self) -> None:
        for contract in self.contracts:
            print(contract.name)

    @printer.command(name="list-contracts")
    def cli(self) -> None:
        pass
```

You’ve just built your first printer. It collects and prints deployable contracts—your first step toward automated contract mapping.

## Tutorial 2: Analyzing Contract Functions

Understanding which functions are externally callable is crucial for security: public ‘withdraw’ or ‘transfer’ functions often define a contract’s attack surface. Let’s create a printer that maps out the attack surface by listing all public and external functions.

### Setting Up the Functions Printer

Create a new printer:

```bash
wake up printer list-functions
```

### Implementation Strategy

Now we’ll expand our printer to map each contract’s external attack surface. Our goal: List only the final, callable public/external functions for each deployable contract, excluding interfaces and overridden functions.

While we could use `visit_function_definition` to iterate all functions, grouping them by contract provides better context. We’ll use `visit_contract_definition` and access the `functions` attribute.

Start by collecting all contracts:

```python
class ListFunctionsPrinter(Printer):

    contracts: list[ir.ContractDefinition] = []

    def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
        self.contracts.append(node)
```

### Processing the Inheritance Hierarchy

In the `print()` method, we traverse the inheritance hierarchy from base to derived contracts, showing callable functions at each level:

```python
   def print(self) -> None:
        for node in self.contracts:

            # Skip if not a contract (interface, library)
            if node.kind != ir.enums.ContractKind.CONTRACT:
                continue

            #Process leaf contracts only
            if len(node.child_contracts) !=0:
                continue

            # Print the inheritance hierarchy (from base to derived)
            for base_contract in reversed(node.linearized_base_contracts):
                print(f"Contract: {base_contract.name}")

                functions = self.get_callable_final_functions(base_contract)

                if len(functions) > 0:
                    print("Functions:")
                    for function in functions:
                        print(f"  {function.name}")
            print("--------------------")
```

### Filtering for Attack Surface Functions

The `get_callable_final_functions` helper method identifies which functions can actually be called by external actors. It checks that a function is a final implementation (not overridden by child contracts) and has public or external visibility. These are the functions that matter for security analysis since they represent the contract’s actual attack surface.

```python
    def get_callable_final_functions(self, contract: ir.ContractDefinition) -> list[ir.FunctionDefinition]:
        return [
            func for func in contract.functions
            if len(func.child_functions) == 0  # Is final implementation
            and func.visibility in [ir.enums.Visibility.PUBLIC, ir.enums.Visibility.EXTERNAL]
        ]
```

### Running the Functions Printer

Execute the printer to see the inheritance hierarchy and callable functions:

```bash
wake print list-functions
```

Output:

```text
Contract: Context
Contract: Ownable
Functions:
  owner
  renounceOwnership
  transferOwnership
Contract: SingleTokenVault
Functions:
  constructor
  deposit
  withdraw
  emergencyWithdraw
  balanceOf
  setDepositLimits
--------------------
Contract: EIP712Example
Functions:
  constructor
  DOMAIN_SEPARATOR
  castVoteBySignature
  getVoteCounts
--------------------
Contract: Context
Contract: IERC20
Contract: IERC20Metadata
Contract: IERC20Errors
Contract: ERC20
Functions:
  name
  symbol
  decimals
  totalSupply
  balanceOf
  transfer
  allowance
  approve
  transferFrom
Contract: IERC20Permit
Contract: IERC5267
Contract: EIP712
Functions:
  eip712Domain
Contract: Nonces
Contract: ERC20Permit
Functions:
  permit
  nonces
  DOMAIN_SEPARATOR
Contract: PermitToken
Functions:
  constructor
--------------------
Contract: Token
Functions:
  constructor
  mintTokens
  transfer
  transferWithBytes
  getBalance
--------------------
Contract: Context
Contract: IERC20
Contract: IERC20Metadata
Contract: IERC20Errors
Contract: ERC20
Functions:
  name
  symbol
  decimals
  totalSupply
  balanceOf
  transfer
  allowance
  approve
  transferFrom
Contract: MockERC20
Functions:
  constructor
--------------------
```

The output gives you a quick visual map of each contract’s inheritance and callable entry points.

## Tutorial 3: Adding Command-Line Options

Real-world analysis often requires focusing on specific contracts. Let’s enhance our printer to accept command-line arguments, allowing targeted analysis of individual contracts.

### Understanding CLI Integration

Wake printers can accept command-line options through the `@click.option` decorator. This enables dynamic analysis based on user input. We’ll add a `--contract-name` option to filter results for a specific contract.

### Implementing the Option

First, add a class member to store the contract name, then use `@click.option` to capture the command-line argument:

```python
@printer.command(name="list-functions")
@click.option("--contract-name", type=str, required=False)
def cli(self, contract_name: str | None) -> None:
    self.contract_name = contract_name
```

### Conditional Filtering Logic

The `print()` method now checks if a specific contract was requested. If no contract name is provided, the printer lists all deployable contracts. If a name is specified, it drills into that contract’s hierarchy only, even if it’s not a leaf contract.

### Complete Implementation with CLI Options

Here’s the final printer with optional contract filtering built in.

```python
from __future__ import annotations

import networkx as nx
import rich_click as click
import wake.ir as ir
import wake.ir.types as types
from rich import print
from wake.cli import SolidityName
from wake.printers import Printer, printer


class ListFunctionsPrinter(Printer):

    contracts: list[ir.ContractDefinition] = []
    contract_name: str | None = None


    def get_callable_final_functions(self, contract: ir.ContractDefinition) -> list[ir.FunctionDefinition]:
        return [
            func for func in contract.functions
            if len(func.child_functions) == 0  # Is final implementatione
            and func.visibility in [ir.enums.Visibility.PUBLIC, ir.enums.Visibility.EXTERNAL]
        ]

    def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
        self.contracts.append(node)


    def print(self) -> None:
        for node in self.contracts:
            # If contract name is specified, only process that contract
            if self.contract_name is not None and node.name != self.contract_name:
                continue

            # Skip if not a contract (e.g., interface, library)
            if node.kind != ir.enums.ContractKind.CONTRACT:
                continue

            # If no contract name specified, only process leaf contracts
            if self.contract_name is None and len(node.child_contracts) != 0:
                continue

            # Print the inheritance hierarchy (from base to derived)
            for base_contract in reversed(node.linearized_base_contracts):
                print(f"Contract: {base_contract.name}")

                functions = self.get_callable_final_functions(base_contract)

                if len(functions) > 0:
                    print("Functions:")
                    for function in functions:
                        print(f"  {function.name}")
            print("--------------------")

    @printer.command(name="list-functions")
    @click.option("--contract-name", type=str, required=False)
    def cli(self, contract_name: str | None) -> None:
        self.contract_name = contract_name
        pass
```

Now your printer can analyze specific contracts on demand—a feature that makes targeted auditing fast and repeatable.

### Using the Enhanced Printer

Now you can analyze specific contracts:

```solidity
# Analyze all deployable contracts
wake print list-functions

# Focus on a specific contract
wake print list-functions --contract-name Token
```

## Practical Applications for Security Auditing

With these foundational skills, you can create printers that visualize and analyze your codebase structure. Custom printers let you visualize key code relationships quickly. They don’t detect vulnerabilities directly, but they reveal patterns that guide your manual review.

### Useful Analysis Patterns to Visualize

Once you’re comfortable building printers, try these advanced patterns to visualize complex security relationships.

**Access Control Mapping** : Create printers that list all state-changing functions alongside their access modifiers. This overview helps you quickly identify which functions might need additional protection.

**Call Flow Visualization** : Map out which contracts call which functions. Understanding these relationships reveals potential attack paths and helps prioritize your audit focus.

**State Variable Usage** : Track how storage variables are accessed across functions. This analysis helps identify complex state dependencies that warrant closer inspection.

**Inheritance Hierarchies** : Visualize the complete inheritance tree of your contracts. Complex inheritance can hide function implementations and create unexpected behaviors.

### Building Your Analysis Toolkit

Start small. Build printers that solve your immediate needs. Each one adds to your personal toolkit. Over time, you’ll develop reusable scripts that make every audit faster.

The Wake printer system’s flexibility means you can adapt your analysis tools to different audit scenarios. Whether you’re mapping upgrade patterns, visualizing DeFi protocol interactions, or understanding storage layouts, custom printers transform hours of manual code reading into seconds of automated analysis and visualization.

## Next Steps

Printers give you the map; detectors find the vulnerabilities. Together, they turn Solidity auditing from a manual grind into a structured, insightful process. Every printer you write makes complex code clearer—and strengthens the safety of the smart contracts you review.

For vulnerability detection, Wake provides a separate detector system that goes beyond visualization to identify actual security issues. Printers give you the map; detectors find the problems.

Consider contributing your printers back to the community. Analysis tools are most powerful when shared, and your custom printer might help another auditor understand complex codebases more efficiently.

For more advanced topics and complete API reference, visit: [Wake static analysis documents](/wake/docs/latest/static-analysis/using-printers/)

Also read [The Beginners Guide for Manually Guided Fuzzing](/research/a-beginners-guide-to-manually-guided-fuzzing/)
