本文介绍了如何使用 Wake 的 printer 系统自动化扫描智能合约中的风险模式。通过结合 Python 的简洁性和 Wake 的中间表示 (IR),可以将复杂的静态分析转化为简单的脚本,从而快速定位潜在的安全漏洞。文章通过创建自定义 printer 的教程,展示了如何实现合约列表、函数分析以及添加命令行选项等功能,旨在提升智能合约审计的效率和准确性。
手动审查 Solidity 代码既缓慢又容易出错。一个被忽略的函数可能隐藏着代价高昂的漏洞。Wake 的打印机系统可以自动搜索这些有风险的模式,将数小时的手动审查变成快速、可靠的扫描。
Wake 打印机结合了 Python 的简洁性和 Wake 的中间表示 (IR),将复杂的静态分析转化为简单的脚本,高亮显示诸如不受限制的提款或缺失的访问控制等问题。本指南将引导你创建自定义打印机,以高亮显示智能合约中与安全相关的模式。
在本教程中,我们将使用 workshop 仓库作为示例项目:
git clone https://github.com/Ackee-Blockchain/2025-workshop-fuzzing
cd 2025-workshop-fuzzing
npm install
Bash
复制
在继续之前,请通过运行以下命令检查 Wake 是否成功编译你的项目:
wake up
Bash
复制
Wake 附带了几个内置打印机,展示了不同类型的分析。你可以使用以下命令列出它们:
wake print
Bash
复制
按名称运行特定的打印机:
wake print storage-layout
Bash
复制
存储布局输出
内置打印机演示了系统的功能,但真正的力量来自于编写针对你的安全分析需求量身定制的自定义打印机。一旦你了解了内置打印机的工作方式,你就会发现使用你自己的分析工具扩展 Wake 是多么容易。在本指南结束时,你将了解如何创建打印机来检测与你的审计方法相关的漏洞模式。
让我们从一个简单的打印机开始,它列出项目中的所有合约。此示例介绍了你将在更复杂的分析中使用的核心概念。
运行以下命令来搭建你的第一个打印机:
wake up printer list-contracts
Bash
复制
Wake 会生成一个新的打印机目录和一个包含以下结构的起始文件:
printers/ 目录list-contracts.py生成的模板提供了以下起始结构:
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
Python
复制
以下是模板中每个部分的作用:
print():显示分析结果的主要执行方法cli():用于自定义参数的命令行界面处理程序Wake 使用访问者模式来遍历合约的抽象语法树 (AST)。访问者模式允许 Wake 自动导航你的代码结构,使你能够对特定元素(例如合约或函数定义)做出反应。
要列出合约,我们将覆盖 visit_contract_definition 方法,该方法会为代码库中的每个合约调用。
将此方法添加到你的 ListContractsPrinter 类:
def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
print(node.name)
Python
复制
测试你的打印机:
wake print list-contracts
Bash
复制
此命令运行你的打印机并打印在你的项目中找到的所有合约名称。
基本实现显示所有合约,包括接口和继承的合约。让我们改进它以仅显示可部署的合约:
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
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)
Python
复制
ContractDefinition 类包含可用于过滤结果的属性。有关完整参考,请参见:https://ackee.xyz/wake/docs/latest/api-reference/ir/declarations/contract-definition/
这是最终版本,具有适当的关注点分离——在遍历期间收集数据并在 print() 方法中显示它:
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
Python
复制
你刚刚构建了你的第一个打印机。它收集并打印可部署的合约——这是你实现自动化合约映射的第一步。
了解哪些函数可从外部调用对于安全至关重要:公共 'withdraw' 或 'transfer' 函数通常定义了合约的攻击面。让我们创建一个打印机,通过列出所有公共和外部函数来绘制攻击面。
创建一个新的打印机:
wake up printer list-functions
Bash
复制
现在,我们将扩展我们的打印机以映射每个合约的外部攻击面。我们的目标:仅列出每个可部署合约的最终、可调用的公共/外部函数,不包括接口和被覆盖的函数。
虽然我们可以使用 visit_function_definition 来迭代所有函数,但按合约对它们进行分组可以提供更好的上下文。我们将使用 visit_contract_definition 并访问 functions 属性。
首先,收集所有合约:
class ListFunctionsPrinter(Printer):
contracts: list[ir.ContractDefinition] = []
def visit_contract_definition(self, node: ir.ContractDefinition) -> None:
self.contracts.append(node)
Python
复制
在 print() 方法中,我们遍历从基合约到派生合约的继承层次结构,显示每个级别的可调用函数:
def print(self) -> None:
for node in self.contracts:
# 跳过如果不是合约(接口、库)
if node.kind != ir.enums.ContractKind.CONTRACT:
continue
#仅处理叶子合约
if len(node.child_contracts) !=0:
continue
# 打印继承层次结构(从基类到派生类)
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("--------------------")
Python
复制
get_callable_final_functions 辅助方法识别哪些函数可以实际被外部参与者调用。它检查一个函数是否是最终实现(未被子合约覆盖)并且具有公共或外部可见性。这些是对于安全分析很重要的函数,因为它们代表了合约的实际攻击面。
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 # 是最终实现\
and func.visibility in [ir.enums.Visibility.PUBLIC, ir.enums.Visibility.EXTERNAL]\
]
Python
复制
执行打印机以查看继承层次结构和可调用函数:
wake print list-functions
Bash
复制
输出:
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
--------------------
Bash
复制
输出为你提供每个合约的继承和可调用入口点的快速可视化地图。
实际分析通常需要关注特定的合约。让我们增强我们的打印机以接受命令行参数,从而可以有针对性地分析单个合约。
Wake 打印机可以通过 @click.option 装饰器接受命令行选项。这可以基于用户输入进行动态分析。我们将添加一个 --contract-name 选项来过滤特定合约的结果。
首先,添加一个类成员来存储合约名称,然后使用 @click.option 来捕获命令行参数:
@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
复制
print() 方法现在检查是否请求了特定的合约。如果没有提供合约名称,则打印机将列出所有可部署的合约。如果指定了名称,它将仅深入到该合约的层次结构中,即使它不是叶子合约也是如此。
这是带有可选合约过滤功能的最终打印机。
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 # 是最终实现e\
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 self.contract_name is not None and node.name != self.contract_name:
continue
# 跳过如果不是合约(例如,接口、库)
if node.kind != ir.enums.ContractKind.CONTRACT:
continue
# 如果未指定合约名称,则仅处理叶子合约
if self.contract_name is None and len(node.child_contracts) != 0:
continue
# 打印继承层次结构(从基类到派生类)
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
Python
复制
现在,你的打印机可以按需分析特定的合约——这一功能使有针对性的审计变得快速且可重复。
现在你可以分析特定的合约:
## 分析所有可部署的合约
wake print list-functions
## 专注于特定的合约
wake print list-functions --contract-name Token
Bash
复制
凭借这些基本技能,你可以创建可视化和分析代码库结构的打印机。自定义打印机使你可以快速可视化关键代码关系。它们不会直接检测漏洞,但它们会揭示指导你手动审查的模式。
一旦你能够熟练地构建打印机,请尝试使用这些高级模式来可视化复杂的安全关系。
访问控制映射:创建列出所有状态更改函数及其访问修饰符的打印机。此概述可帮助你快速识别哪些函数可能需要额外的保护。
调用流程可视化:绘制出哪些合约调用哪些函数。了解这些关系可以揭示潜在的攻击路径,并帮助你确定审计的优先级。
状态变量使用:跟踪如何在函数中访问存储变量。此分析有助于识别需要更仔细检查的复杂状态依赖关系。
继承层次结构:可视化合约的完整继承树。复杂的继承可能会隐藏函数实现并产生意外的行为。
从小处着手。构建能够满足你迫切需求的打印机。每一个都会添加到你的个人工具包中。随着时间的推移,你将开发出可重用的脚本,从而加快每次审计的速度。
Wake 打印机系统的灵活性意味着你可以使你的分析工具适应不同的审计场景。无论你是在绘制升级模式、可视化 DeFi 协议交互还是了解存储布局,自定义打印机都可以将数小时的手动代码阅读转换为数秒的自动化分析和可视化。
打印机为你提供地图;检测器查找漏洞。它们共同将 Solidity 审计从手动苦力转变为结构化、有洞察力的过程。你编写的每个打印机都可以使复杂的代码更清晰——并增强你审查的智能合约的安全性。
对于漏洞检测,Wake 提供了一个单独的检测器系统,该系统超越了可视化来识别实际的安全问题。打印机为你提供地图;检测器查找问题。
考虑将你的打印机贡献回社区。分析工具在共享时最强大,你的自定义打印机可能会帮助其他审计员更有效地理解复杂的代码库。
有关更高级的主题和完整的 API 参考,请访问:Wake 静态分析文档
另请阅读 手动引导模糊测试的初学者指南
- 原文链接: ackee.xyz/blog/mastering...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!