Skip to main content
代理部署由两个合约组成:代理,负责委托每次调用;以及实现,包含逻辑。两者都必须分别验证。对于任何遵循 ERC-1967 的代理,Tenderly 浏览器都会读取标准的实现存储槽,因此一旦两个合约都被验证,对代理地址的调用就会使用实现的 ABI 进行解码。 本指南在 Virtual Environment 上将 UUPS 实现(CounterV1)部署在 ERC1967Proxy 之后,在一次 forge script 运行中验证两者,将实现升级到 CounterV2,并涵盖对未验证部署的代理进行重新验证。对于 Hardhat 的等效方法,请参见验证代理合约

开始之前

  • 与基础库一起安装 OpenZeppelin 的可升级合约:
  • 将两个 remappings 添加到 remappings.txt
remappings.txt
这两个库担任不同的角色:openzeppelin-contracts 提供代理(ERC1967Proxy),而 openzeppelin-contracts-upgradeable 提供实现的构建块(InitializableUUPSUpgradeableOwnableUpgradeable)。可升级版本使用初始化函数替代构造函数,因为当状态位于代理之后时,构造函数不会运行。

实现合约

src/CounterV1.sol
此合约中对代理模式重要的三个部分:
  1. constructor() { _disableInitializers(); } 锁定实现。只有代理应该持有状态;此构造函数使得对实现地址直接调用 initialize 会 revert。
  2. initialize(...) 替代构造函数。代理在部署代理的同一笔交易中对它进行一次 delegatecall。
  3. _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
然后通过代理调用实现的一个函数:
在 Tenderly Dashboard 中,代理的地址页面会显示已验证的 ERC1967Proxy 源代码、带有已链接实现地址的代理标签,以及包含实现函数的 ABI 视图。调用 proxy.increment()proxy.setNumber(...) 的交易在调用跟踪中会使用实现的函数名和参数标签进行解码。
如果浏览器显示代理,但没有使用实现的 ABI 解码调用,最常见的原因是两个合约中只有一个被验证。请对缺失的那个重新运行 forge verify-contract

升级实现

UUPS 升级通过继承自 UUPSUpgradeableupgradeToAndCall(newImpl, callData) 发起。添加一个 V2:
src/CounterV2.sol
编写 V2 时的存储布局约束:
  • 继承自 V1,或原样复制 V1 的存储布局。存储槽是位置相关的;在现有变量之前添加新的状态变量会破坏代理的状态。
  • 新的状态变量放在 V1 现有变量之后,永远不要放在它们之间。
  • 不要更改现有变量的类型或重新排序声明。
script/UpgradeProxy.s.sol
使用相同的 verify 标志运行它:
showLineNumbers
只有 CounterV2 被部署和验证(输出报告 (1) contracts)。代理地址不变;只有它的 ERC-1967 槽发生变化。脚本返回后:
showLineNumbers
浏览器中的代理地址现在按照 CounterV2 的 ABI 进行解码。旧的 CounterV1 实现在其原地址上仍然处于已验证状态,但不再是活动的实现。

验证已经部署的代理

如果您在部署时没有使用 --verify,请为每个合约分别运行 forge verify-contract 实现是通过 new CounterV1() 部署的,没有构造函数参数,因此不需要 --constructor-args 标志:
showLineNumbers
代理需要部署时使用的完全相同的构造函数参数,且经过 ABI 编码:
showLineNumbers
需要正确处理的三个细节:
  1. 合约路径使用 lib/ 位置,而不是 src/。代理是 OpenZeppelin 的代码,因此验证器要匹配的源代码位于 lib/openzeppelin-contracts/... 中。forge script --verify 会自动解析此路径;forge verify-contract 不会。
  2. initData 必须与部署时的 initData 完全匹配。 即使部署后状态发生变化,构造函数参数依然是代理字节码构造时使用的原始 initialize(owner, 42) calldata。
  3. 代理的构造函数签名是 (address, bytes)forge verify-contract 期望已经过 ABI 编码的形式,这正是 cast abi-encode 生成的。

后续步骤