CounterV1)部署在 ERC1967Proxy 之后,在一次 forge script 运行中验证两者,将实现升级到 CounterV2,并涵盖对未验证部署的代理进行重新验证。对于 Hardhat 的等效方法,请参见验证代理合约。
开始之前
- 完成部署和验证合约中的设置:一个 Virtual Environment、一个已注资的部署者地址以及一个包含
cbor_metadata = true和bytecode_hash = "ipfs"的foundry.toml。 - 设置环境变量:
- 与基础库一起安装 OpenZeppelin 的可升级合约:
- 将两个 remappings 添加到
remappings.txt:
remappings.txt
openzeppelin-contracts 提供代理(ERC1967Proxy),而 openzeppelin-contracts-upgradeable 提供实现的构建块(Initializable、UUPSUpgradeable、OwnableUpgradeable)。可升级版本使用初始化函数替代构造函数,因为当状态位于代理之后时,构造函数不会运行。
实现合约
src/CounterV1.sol
constructor() { _disableInitializers(); }锁定实现。只有代理应该持有状态;此构造函数使得对实现地址直接调用initialize会 revert。initialize(...)替代构造函数。代理在部署代理的同一笔交易中对它进行一次 delegatecall。_authorizeUpgrade(...)是必需的 UUPS 权限钩子。每次upgradeToAndCall时都会运行;如果它 revert,升级就会被阻止。
部署脚本
script/DeployProxy.s.sol
abi.encodeCall 构造 initialize calldata(它在编译时对参数进行类型检查,与 abi.encodeWithSignature 不同),然后部署代理。代理的构造函数将实现地址存储在 ERC-1967 实现槽中,并对其进行 delegatecall initData,原子性地初始化代理的存储。
部署并验证两个合约
代理和实现是一个脚本中的两个合约,因此命令与任何多合约部署使用的forge script 形式相同:
showLineNumbers
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy,即来自您 lib/ 树中的 OpenZeppelin 合约,而不是 src/ 中的合约。Foundry 会自动解析此路径,因为脚本的广播日志记录了每一个 new 及其字节码。
确认代理链接
当代理和实现都通过验证时,浏览器会通过代理解码调用。要确认代理正在委托,请直接读取 ERC-1967 实现槽:showLineNumbers
ERC1967Proxy 源代码、带有已链接实现地址的代理标签,以及包含实现函数的 ABI 视图。调用 proxy.increment() 或 proxy.setNumber(...) 的交易在调用跟踪中会使用实现的函数名和参数标签进行解码。
如果浏览器显示代理,但没有使用实现的 ABI 解码调用,最常见的原因是两个合约中只有一个被验证。请对缺失的那个重新运行
forge verify-contract。升级实现
UUPS 升级通过继承自UUPSUpgradeable 的 upgradeToAndCall(newImpl, callData) 发起。添加一个 V2:
src/CounterV2.sol
- 继承自 V1,或原样复制 V1 的存储布局。存储槽是位置相关的;在现有变量之前添加新的状态变量会破坏代理的状态。
- 新的状态变量放在 V1 现有变量之后,永远不要放在它们之间。
- 不要更改现有变量的类型或重新排序声明。
script/UpgradeProxy.s.sol
showLineNumbers
CounterV2 被部署和验证(输出报告 (1) contracts)。代理地址不变;只有它的 ERC-1967 槽发生变化。脚本返回后:
showLineNumbers
CounterV2 的 ABI 进行解码。旧的 CounterV1 实现在其原地址上仍然处于已验证状态,但不再是活动的实现。
验证已经部署的代理
如果您在部署时没有使用--verify,请为每个合约分别运行 forge verify-contract。
实现是通过 new CounterV1() 部署的,没有构造函数参数,因此不需要 --constructor-args 标志:
showLineNumbers
showLineNumbers
- 合约路径使用
lib/位置,而不是src/。代理是 OpenZeppelin 的代码,因此验证器要匹配的源代码位于lib/openzeppelin-contracts/...中。forge script --verify会自动解析此路径;forge verify-contract不会。 initData必须与部署时的initData完全匹配。 即使部署后状态发生变化,构造函数参数依然是代理字节码构造时使用的原始initialize(owner, 42)calldata。- 代理的构造函数签名是
(address, bytes),forge verify-contract期望已经过 ABI 编码的形式,这正是cast abi-encode生成的。
后续步骤
- 部署和验证合约:在 Virtual Environment 上的基础 Foundry 与 Hardhat 验证流程。
- 使用 Foundry 验证智能合约:通过 Tenderly 的验证 API 在公共网络上验证合约。
- 验证代理合约:针对 UUPS、Transparent 和 Beacon 代理的 Hardhat 插件流程。