升级过程是生产环境中Bug的藏身之处,例如遗漏的初始化函数、错误的管理员权限设置或损坏的存储布局。代理模式允许我们升级智能合约,但也引入了传统测试方法难以覆盖的复杂性。基于Python的Wake测试框架,可以帮助我们在这些问题部署到主网之前发现它们。
这使得测试代码保持简洁。通过代理地址调用实现合约的函数变得非常简单:
contract = ExampleERC20Upgradeable(proxy)
接下来,我们将详细介绍如何在Wake中安全地测试代理合约。
1. 导入代理合约
Wake需要编译你的代理合约以生成对应的Python类型绑定。如果代理合约位于项目默认排除的目录中,Wake将不会编译它。
在 wake.toml 配置文件中,默认的排除路径配置为 exclude_paths = ["script", ".venv", "venv", "node_modules", "lib", "test"]。这意味着,除非从非排除目录的文件中导入,否则这些路径下的合约不会被编译。
为了让合约在项目中可用,你需要从一个未被排除的路径导入它。具体做法是,创建一个 tests/imports.sol 文件来引入所需的代理合约,从而使pytypes可用:
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
2. 在Python中导入代理类型
完成上一步后,再次运行 wake up 命令进行编译。Wake会为你的实现合约和代理合约生成Python绑定。接着,在你的测试文件中导入它们:
tests/test_upgradable.py
from pytypes.contracts.ExampleERC20Upgradeable import ExampleERC20Upgradeable
from pytypes.openzeppelin.contracts.proxy.ERC1967.ERC1967Proxy import ERC1967Proxy
3. 部署与初始化合约
首先部署实现合约,然后创建一个指向该实现合约地址的代理。代理合约的初始化数据(_data)需要编码对实现合约 initialize 函数的调用:
@chain.connect()
@on_revert(revert_handler)
def test_default():
impl_erc20 = ExampleERC20Upgradeable.deploy()
proxy = ERC1967Proxy.deploy(
implementation=impl_erc20,
_data=abi.encode_call(ExampleERC20Upgradeable.initialize, ("Upgradable Token", "UPG", 10**20, chain.accounts[0])),
from_=chain.accounts[0]
)
这里的 _data 参数编码了在代理部署期间执行的初始化调用,它取代了非可升级合约中使用的构造函数模式。
4. 通过代理地址访问实现函数
将代理合约的地址用实现合约的类进行包装。这会指示Wake通过代理路由所有的函数调用,同时使用实现合约的ABI进行交互:
contract = ExampleERC20Upgradeable(proxy)
Wake会自动处理底层的delegatecall调用路由,因此你可以像与一个普通部署的合约一样与这个包装后的对象进行交互。
5. 调用实现合约的函数
现在,所有实现合约的函数都可以通过这个包装后的代理对象来调用。你可以验证合约行为、检查事件并测试状态变更:
# 验证初始余额
assert contract.balanceOf(chain.accounts[1]) == 0
# 执行转账交易
tx = contract.transfer(chain.accounts[1], 10**18, from_=chain.accounts[0])
# 检查发出的事件
event = next(event for event in tx.events if isinstance(event, ExampleERC20Upgradeable.Transfer))
assert event.from_ == chain.accounts[0].address
assert event.to == chain.accounts[1].address
assert event.value == 10**18
# 验证更新后的余额
assert contract.balanceOf(chain.accounts[1]) == 10**18
这个测试验证了代理能够正确地委托调用实现合约,并且状态得以按预期维护。
结论
Wake通过其Python绑定极大地简化了代理合约的测试流程。你只需要用实现合约类包装代理地址,然后直接调用函数即可。这种方法同样适用于单元测试和手动引导模糊测试,使得你可以用测试标准合约的同一套工具来测试可升级合约。
这有助于在升级导致的Bug(如遗漏的初始化、存储冲突、访问控制问题)演变为安全漏洞之前将其捕获。请用测试其他所有内容的方式来测试你的代理模式。
附录:完整测试代码
import math
from wake.testing import *
from dataclasses import dataclass
from pytypes.contracts.ExampleERC20Upgradeable import ExampleERC20Upgradeable
from pytypes.openzeppelin.contracts.proxy.ERC1967.ERC1967Proxy import ERC1967Proxy
# 打印失败交易的调用跟踪
def revert_handler(e: RevertError):
if e.tx is not None:
print(e.tx.call_trace)
@chain.connect()
@on_revert(revert_handler)
def test_default():
impl_erc20 = ExampleERC20Upgradeable.deploy()
proxy = ERC1967Proxy.deploy(
implementation=impl_erc20,
_data=abi.encode_call(ExampleERC20Upgradeable.initialize, ("Upgradable Token", "UPG", 10**20, chain.accounts[0])),
from_=chain.accounts[0]
)
contract = ExampleERC20Upgradeable(proxy) # 用合约类包装代理地址以调用函数
assert contract.balanceOf(chain.accounts[1]) == 0
tx = contract.transfer(chain.accounts[1], 10**18, from_=chain.accounts[0])
event = next(event for event in tx.events if isinstance(event, ExampleERC20Upgradeable.Transfer))
assert event.from_ == chain.accounts[0].address
assert event.to == chain.accounts[1].address
assert event.value == 10**18
assert contract.balanceOf(chain.accounts[1]) == 10**18