Prerequisites and Setup
In this tutorial, we will use the workshop repository as an example project:
wake up
Bash
Copy
wakeprint
Bash
Copy
Copy
Copy
clickimport wake.ir as irimport wake.ir.types as typesfrom rich importprintfrom wake.cli import SolidityNamefrom wake.printers import Printer, printerclass ListContractsPrinter(Printer):def print(self) -> None:pass @printer.command(name="list-contracts")def cli(self) -> None:pass
Python
Copy
The following is the function of each part in the template:
Implementing the Visitor Pattern
Wake uses the Visitor pattern to traverse the abstract syntax tree (AST) of contracts. The Visitor pattern allows Wake to automatically navigate your code structure, enabling you to react to specific elements such as contract or function definitions.
To list the contracts, we will override the `visit_contract_definition` method, which will be called for each contract in the codebase. Add this method to your ListContractsPrinter class: wake print list-contracts
Bash
Copy
This command runs your printer and prints all contract names found in your project.
Improved Output
Basic implementation displays all contracts, including interfaces and inherited contracts.
The basic ... Let's improve it to show only deployable contracts:
def visit_contract_definition(self, node: ir.ContractDefinition) -> None:iflen(node.child_contracts) != 0:returnif node.kind != ir.enums.ContractKind.CONTRACT:returnprint(node.name)
Python
Copy
The ContractDefinition class contains properties that can be used to filter the results. For a complete reference, please see: https://ackee.xyz/wake/docs/latest/api-reference/ir/declarations/contract-definition/ Full Implementation This is the final version with proper separation of concerns—collecting data during traversal and displaying it in the `print()` method: Tutorial 2: Analyzing Contract Functions Understanding which functions can be called from outside is crucial for security: public 'withdraw' or 'transfer' functions often define the attack surface of a contract. Let's create a printer to plot the attack surface by listing all public and external functions.
Set function printer
Create a new printer:
class ListFunctionsPrinter(Printer): contracts: list[ir.ContractDefinition] = []def visit_contract_definition(self, node: ir.ContractDefinition) -> None: `self.contracts.append(node)` (Python) (Copy) (Handling Inheritance Hierarchy) (In the `print()` method, we iterate through the inheritance hierarchy from the base contract to the derived contract, displaying the callable functions at each level:) (def) (get_callable_final_functions(self, contract: ir.ContractDefinition) -> list[ir.FunctionDefinition]:return [\ funny == 0# Is the final implementation\and func.visibility in [ir.enums.Visibility.PUBLIC, ir.enums.Visibility.EXTERNAL]\ ]
Python
Copy
Run function printer
Execute the printer to see the inheritance hierarchy and callable functions:
Contract: ContextContract: OwnableFunctions: owner renounceOwnership transferOwnershipContract: SingleTokenVaultFunctions: constructor deposit withdraw emergencyWithdraw balanceOf setDepositLimits--------------------Contract: EIP712ExampleFunctions: constructor DOMAIN_SEPARATOR castVoteBySignature getVoteCounts--------------------------Contract: ContextContract: IERC20Contract: IERC20MetadataContract: IERC20ErrorsContract: ERC20Functions: name symbol decimals totalSupply balanceOf transfer allowance approve transferFromContract: IERC20PermitContract: IERC5267Contract: EIP712Functions: eip712DomainContract: NoncesContract: ERC20PermitFunctions: permit nonces DOMAIN_SEPARATORContract: PermitTokenFunctions: constructor--------------------------Contract: TokenFunctions: constructor mintTokens transfer transferWithBytes getBalance--------------------------Contract: ContextContract: IERC20Contract: IERC20MetadataContract: IERC20ErrorsContract: ERC20Functions: name symbol decimals totalSupply balanceOf transfer allowance approve transferFromContract: MockERC20Functions: constructor--------------------
Bash
Copy
Output provides you with a quick visual map of the inheritance and callable entry points for each contract.
Output provides you with a quick visual map of the inheritance and callable entry points for each contract.
@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
Python
Copy
Conditional Filtering Logic
print() The method now checks if a specific contract has been requested. If no contract name is provided, the printer will list all deployable contracts. If a name is specified, it will only delve into the hierarchy of contracts for that name, even if it is not a leaf contract.
Full Implementation with CLI Options
This is the final printer with optional contract filtering. ...
## Analyze all deployable contractswakeprint list-functions## Focus on a specific contractwakeprint list-functions --contract-name Token
Bash
Copy
Subsequent steps
Printers provide the map; detectors find vulnerabilities. Together, they transform Solidity auditing from manual drudgery into a structured, insightful process. Each printer you write can make complex code clearer—and enhance the security of the smart contracts you audit. For vulnerability detection, Wake offers a separate detector system that goes beyond visualization to identify actual security issues. Printers provide the map; detectors find the problems. Consider contributing your printers back to the community. Analysis tools are most powerful when shared, and your custom printers can help other auditors understand complex codebases more effectively.