首页
/ Union EVM 合约部署全解:CREATE3 确定性地址、UUPS 可升级代理与多链合约验证

Union EVM 合约部署全解:CREATE3 确定性地址、UUPS 可升级代理与多链合约验证

2026-09-05 13:31:32作者:苗圣禹Peter

本文围绕 evm/README.md 展开,讲解 Union 项目在 EVM 链上部署 IBC 合约栈的完整方法论:通过 CREATE3 部署器实现“不含 initcode”的确定性地址推导,用 ERC1967 + UUPS 代理模式支撑跨网地址一致的合约升级,并给出地址预计算、nix run 部署/升级命令、面向 Etherscan/Blockscout/Routescan 等多浏览器验证的可复制操作。读完你能够独立预测任意网络上的合约地址、理解 devnet 与生产地址差异的根因,并掌握升级后重新验证合约的标准流程。

1. 部署体系概览

Union 的 EVM 侧部署包含三类信息(见 evm/README.md):

  1. 各链实际部署地址:已迁移到协议文档 deployments 页面,其数据源是 deployments/deployments.json——按 universal chain ID(UCS-04,如 arbitrum.42161ethereum.1)组织,记录每链的 deployer、sender、各合约地址、部署区块高度与源码 commit。
  2. 本地 devnet 浏览器地址:仅在 x86 机器上运行本地以太坊 devnet 时有效(Blockscout 当前不支持 arm64),地址见下表。
  3. 部署流程:所有已部署合约均为可升级代理,经专用 Deployer 合约做 CREATE3 确定性部署,并配套 nix 打包的部署/升级/验证命令。

1.1 Devnet 浏览器地址

本地 devnet(devnet 配置定义于 evm/evm.nix,chain-id 32382,RPC 指向 http://localhost:8545)部署后的合约地址可在 Blockscout 中查看:

类别 名称 地址
core IBCHandler 0xed2af2aD7FE0D92011b26A2e5D1B4dC7D12A47C5
light-clients CometblsClient 0xc4f27a952faBa4174ce0Ee6D9d0c6F4c41524d49
apps UCS00 0x21bd17aec8CEb789D3145a606968Dcc428c1e4F4
UCS01 0xa9d03ba6E27B43c69a64C87F845485b73A8e5d46
UCS02 0x524D4d28fc90dc5A257162abE37081f52681C7D6
support Multicall 0x9fd9D9528c8373D990a1380B9414bDE179007A35

1.2 为什么 devnet 地址会经常变化

原文档给出一条关键说明:“我们经常在推出存储破坏性(storage breaking)更新时直接重新部署而不是升级,因此地址会不同;生产合约只会通过代理升级,跨网络保持相同地址。”

从源码结构看,这套机制的落地方式正是 CREATE3 代理:每次 deployIfNotExists 都是用新的实现合约地址 + 初始化调用构造一个 ERC1967Proxy 的创建码(见 evm/scripts/Deploy.s.solUnionScript.deploytype(ERC1967Proxy).creationCode 的封装)。由于 CREATE3 地址推导与创建码(initcode)无关,devnet 重部署后即使实现字节码变了,代理地址仍可由 (deployer, sender, salt) 唯一确定;而生产环境只走 UUPS 升级,代理地址天然恒定。

2. CREATE3 部署器:不含 initcode 的确定性地址

2.1 Deployer 合约

为了生成确定性地址,Union 使用一个特化的部署器合约,其原理与 solady 库的 CREATE3 一致:地址推导中完全不包含任何合约的字节码哈希。仓库内实际部署器实现见 evm/scripts/Deployer.sol

import "solady/utils/CREATE3.sol";
import "solady/utils/LibString.sol";

contract Deployer {
    using LibString for *;

    function deploy(
        string memory salt,
        bytes calldata creationCode,
        uint256 value
    ) public returns (address) {
        return CREATE3.deployDeterministic(
            value,
            creationCode,
            keccak256(abi.encodePacked(msg.sender.toHexString(), "/", salt))
        );
    }
}

要点:

  • 最终 salt 是 keccak256(sender 地址字符串 + "/" + 业务 salt),即同一个部署器下,不同发送者即使使用相同业务 salt 也不会地址碰撞evm/scripts/Deploy.s.solgetDeployed 用同一公式 CREATE3.predictDeterministicAddress(keccak256(sender/"salt"), deployer) 做预测)。
  • 该 Deployer 会预先部署在所有部署 IBC 栈的 EVM 网络上,部署动作本身用 cast compute-address --nonce 0 <SOURCE> 即可从部署 Deployer 的源账户(nonce 为 0)预计算。
  • 部署器由 DeployDeployer 脚本合约发出(evm/scripts/Deploy.s.soldeployDeployer() 就是简单 new Deployer())。

2.2 salt 与合约的映射

原文档给出的 salt 映射表如下(表中为文档版本;实际 salt 常量在 evm/scripts/Deploy.s.solLIB_SALT / IBC_SALT / LIGHT_CLIENT_SALT / Protocols 库中定义,且比文档表更全):

salt 合约
ibc-is-based IBCHandler(在 deployments/deployments.json 中记录为 core
lightclients/cometbls CometblsClient
protocols/ucs01 UCS01
protocols/ucs02 UCS02
multicall Multicall

当前脚本源码中实际使用的 salt 集合(比上表更完整):

  • 核心/管理ibc-is-based(IBCHandler)、manager(Manager,权限中枢);
  • 轻客户端lightclients/cometblslightclients/loopbacklightclients/state-lens/ics23/mptlightclients/state-lens/ics23/ics23lightclients/state-lens/ics23/smtlightclients/proof-lens
  • 应用protocols/ucs00(PingPong)、protocols/ucs03(Zkgm);
  • 支持合约lib/multicall-v2(Multicall 代理)、lib/zkgm-erc20-v2(ZkgmERC20 实现)、lib/proxy-account-v1(ProxyAccount 实现)。

由此,最终地址完全由 (deployer 源码, deployer 创建 nonce, deployer 地址, sender, salt) 五元组决定——与上面任何合约的字节码哈希无关。这意味着你可以在部署前、甚至在其他链上,提前算出即将部署的合约地址,这对跨链握手(需要预先知道对端 IBCHandler 地址)尤其重要。

2.3 在其他网络上预计算地址

原文档给出的完整预计算流程(可直接复制执行):

# 1) 用全新账户 <SOURCE>(nonce 0)部署 deployer 时的地址
cast compute-address --nonce 0 <SOURCE>
# 得到 <DEPLOYER>
# 2) 给定 <DEPLOYER> 与 <SENDER>,计算 IBC 栈全部地址
nix run .#evm-contracts-addresses -- <DEPLOYER> <SENDER>

文档中用 devnet 私有密钥给出的真实示例(源账户 0xBe68fC2d8249eb60bfCf0e71D5A0d2F2e292c4eD):

$ cast compute-address --nonce 0 0xBe68fC2d8249eb60bfCf0e71D5A0d2F2e292c4eD
Computed Address: 0x86D9aC0Bab011917f57B9E9607833b4340F9D4F8
$ nix run .#evm-contracts-addresses -- 0x86D9aC0Bab011917f57B9E9607833b4340F9D4F8 0xBe68fC2d8249eb60bfCf0e71D5A0d2F2e292c4eD

Script ran successfully.
Gas used: 52087

== Logs ==
  IBCHandler: 0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5
  CometblsClient: 0xc4f27a952faba4174ce0ee6d9d0c6f4c41524d49
  UCS01: 0xa9d03ba6e27b43c69a64c87f845485b73a8e5d46
  UCS02: 0x524d4d28fc90dc5a257162abe37081f52681c7d6

这些输出与 1.1 节 devnet 浏览器地址完全一致,可作为该推导公式的正确性验证。

从源码看,evm-contracts-addresses 命令(evm/evm.nix 中的 evm-contracts-addresses 包)实际执行的是 evm/scripts/Deploy.s.sol 里的 GetDeployed 脚本:它对每个 salt 调用 CREATE3.predictDeterministicAddress 预测地址,再读取链上 ERC1967 实现槽(implOf 通过 vm.load(x, ERC1967Utils.IMPLEMENTATION_SLOT) 取实现地址),最终把“代理 + 实现 + 构造参数”的完整清单写入 contracts.json。这个清单随后被全量验证命令(verify-all-contracts)消费,用于逐合约 forge verify-contract --constructor-args

3. 可升级代理:ERC1967 代理 + UUPS 升级

原文档声明:“所有已部署合约都是可升级代理,把调用转发到底层实现。” 从仓库源码可以得到更完整的图景:

3.1 代理层与实现层

  • 代理:所有业务合约都部署为 OpenZeppelin ERC1967Proxy。部署脚本在 evm/scripts/Deploy.s.soldeploy() 中把 type(ERC1967Proxy).creationCodeabi.encode(实现地址, 初始化调用) 打包后经 Deployer 发出,例如 IBCHandler 的初始化是 IBCHandler.initialize(manager)
  • 实现:实现合约继承 OZ upgradeable 的 Initializable / UUPSUpgradeable。例如 evm/contracts/Manager.solcontract Manager is Initializable, UUPSUpgradeable, AccessManagerUpgradeable,即权限中枢本身就是可 UUPS 升级的。

3.2 部署与角色初始化(DeployIBC

DeployIBC 脚本(由 nix 命令 eth-deploy-<chain> 调用)执行顺序为:Manager → IBCHandler → 五个轻客户端(Cometbls、StateLens MPT/ICS23/SMT、ProofLens)→ UCS00 → ZkgmERC20/ProxyAccount/UCS03 → Multicall,随后 setupRoles 授权:

  • RELAYER 角色:绑定到 IBCHandler/Multicall 上一组函数选择器,包括 registerClientcreateClientupdateClientmisbehaviour 以及 batchSendbatchAcksrecvPacketacknowledgePackettimeoutPacket 等中继函数——即中继者只能调用这些白名单函数;
  • PAUSER / UNPAUSER 角色:绑定 CometblsClient 与 UCS03 的 pause/unpause
  • RATE_LIMITER 角色:绑定 UCS03 的 setBucketConfig
  • owner 被授予全部角色,且 Multicall 也被授予 RELAYER 以便中继者经其批量调用。

全部合约部署完成后,registerClientcometblsstate-lens/ics23/* 等客户端类型注册进 IBCHandler。整套逻辑可在 evm/scripts/Deploy.s.solUnionScript.deployIBCsetupRoles 中逐行核对。

4. nix 命令体系:地址、部署与验证

所有运维命令由 evm/evm.nixnix run 应用的形式生成,并按链名(devnet、sepolia、holesky、bob、corn、ethereum、bsc、base、arbitrum、sei 等)挂到 evm-scripts.<chain>.* 下。

4.1 预计算地址

nix run .#evm-contracts-addresses -- <DEPLOYER> <SENDER>

4.2 部署

eth-deploy-<chain> 需要两个必填参数 --deployer_pk(Deployer 合约地址)与 --sender_pk(通过 Deployer 创建合约的发送者地址),底层执行:

forge script scripts/Deploy.s.sol:DeployIBC -vvvv --rpc-url <RPC> --broadcast

并注入 DEPLOYERSENDERWETH_ADDRESSNATIVE_TOKEN_NAME/SYMBOL/DECIMALSRATE_LIMIT_ENABLED 等环境变量(UCS03 参数来源,见 UnionBase.getUCS03Params)。此外还有 eth-deploy-deployer-and-ibc-<chain>(首次部署:先 new Deployer() 再走 DeployDeployerAndIBC),以及针对单个合约的 eth-deploy-single-<kind>(如 multicallcometbls-clienterc20uz-asset 等,evm/evm.nixdeploy-single 生成的应用名)。

可复现编译配置:为保证任意机器编译出可验证的相同字节码,Foundry 配置固定为(见根目录 foundry.toml,与 evm/evm.nixfoundryConfig 一致):

[profile.default]
solc_version   = "0.8.27"
via_ir         = true
optimizer      = true
optimizer_runs = 10_000
bytecode_hash  = "none"      # 字节码不含 hash,保证跨环境一致
cbor_metadata  = false       # 不附加元数据 CBOR
ast            = true

4.3 升级合约

查看所有可生成的升级脚本:

nix run .#eth-upgrade- <TAB>

evm/evm.nix 的 upgrade 构建逻辑看,每个链 × 每个协议都会生成 upgrade-<name>upgrade-<name>-dry(dry-run 需要额外 --owner_pk--dry_url 参数,用 vm.prank 在 fork 上模拟)以及 safe-upgrade-<name>(经 Safe 多签提案,见 evm/scripts/Deploy.s.solBaseUpgradesafe.proposeTransaction 的调用)三种应用;协议覆盖 core(IBCHandler)、ucs00ucs03(含 ucs03-v1-to-v2 迁移脚本)、cometbls-client、各 state-lens/proof-lens/loopback 客户端、u/eu/udrop/z-asset 等。

原文档给出的执行示例:

nix run .\#eth-upgrade-holesky-ucs03 -- \
  --deployer_pk 0xa3cd41bff71ad19fddfd901a9773c975a0404d97 \
  --sender_pk 0x153919669Edc8A5D0c8D1E4507c9CE60435A1177 \
  --private_key omitted

升级的底层动作是 UUPSUpgradeable(target).upgradeToAndCall(newImplementation, upgradeCall)——即升级只替换 ERC1967 实现槽,代理地址不变(evm/scripts/Deploy.s.sol BaseUpgrade.run)。

5. 合约验证:Tenderly 默认 + 多浏览器覆盖

原文档说明:“默认情况下我们在 Tenderly 上验证所有合约(支持链);也可以通过 FOUNDRY_ETHERSCANVERIFIER 环境变量接入其他验证器。”

evm/evm.nix 可确认其实现机制:setupFoundryVerifcationVars 函数为每条链设置默认验证端点(Tenderly 的 https://api.tenderly.co/.../verify/network/<chain-id>/public),并允许外部 FOUNDRY_ETHERSCAN / VERIFIER 环境变量覆盖。FOUNDRY_ETHERSCAN 采用 Foundry 的 nix 风格配置字面量 { chain = { key = "...", chain = "1", url = "..." } }

5.1 升级后重新验证

nix run .\#eth-verify-holesky DEPLOYER_ADDR SENDER_ADDR ETHERSCAN_API_KEY
# 例如:
nix run .\#eth-verify-holesky 0xa3cd41bff71ad19fddfd901a9773c975a0404d97 0x153919669Edc8A5D0c8D1E4507c9CE60435A1177 omitted

eth-verify-<chain> 内部执行 forge verify-contract --force --watchwith-verify-flag = false,即单独验证场景不带 --verify 参数)。注意一条来自源码的注释:验证代理合约时必须针对实现合约,实现地址可用 cast impl $ADDRESS -r $RPC_URL 获取。另有 verify-against-commit 变体:按指定 git commit 构建源码后用 forge verify-bytecode 验证历史版本。

5.2 其他受支持浏览器的完整命令

原文档列出(非穷尽)以下浏览器及覆盖验证的完整命令,均可直接复用:

ethereum.1 — Etherscan:

# 将 $KEY 替换为你的 etherscan api key
FOUNDRY_ETHERSCAN='{ chain = { key = "$KEY", chain = "1", url = "https://api.etherscan.io/api" } }' nix run .#evm-scripts.ethereum

ethereum.1 — Blockscout(key 可为空,但 foundry 配置 schema 仍要求该字段):

VERIFIER=blockscout FOUNDRY_ETHERSCAN='{ chain = { key = "", chain = "1", url = "https://eth.blockscout.com/api" } }' nix run .#evm-scripts.ethereum

ethereum.1 — Routescan:

FOUNDRY_ETHERSCAN='{ chain = { key = "verifyContract", chain = "1", url = "https://api.routescan.io/v2/network/mainnet/evm/1/etherscan" } }' nix run .#evm-scripts.ethereum

bob.60808

VERIFIER=blockscout FOUNDRY_ETHERSCAN='{ chain = { key = "", chain = "60808", url = "https://explorer.gobob.xyz/api" } }' nix run .#evm-scripts.bob

bob.808813

VERIFIER=blockscout FOUNDRY_ETHERSCAN='{ chain = { key = "", chain = "808813", url = "https://bob-sepolia.explorer.gobob.xyz/api" } }' nix run .#evm-scripts.bob-sepolia

corn.21000000 / corn.21000001

FOUNDRY_ETHERSCAN='{ chain = { key = "verifyContract", chain = "21000000", url = "https://api.routescan.io/v2/network/mainnet/evm/21000000/etherscan" } }' nix run .#evm-scripts.corn
FOUNDRY_ETHERSCAN='{ chain = { key = "verifyContract", chain = "21000001", url = "https://api.routescan.io/v2/network/testnet/evm/21000001/etherscan" } }' nix run .#evm-scripts.corn-testnet

sei.1328(Seitrace,key 可填任意非空字符串):

FOUNDRY_ETHERSCAN='{ chain = { key = "asdf", chain = "1328", url = "https://seitrace.com/atlantic-2/api" } }' nix run .#evm-scripts.sei-atlantic

5.3 全量验证

verify-all-contractsnix run .#evm-scripts.<chain>.verify-all-contracts)的流程(evm/evm.nix):先跑 GetDeployed 生成 contracts.json,再用 jq 遍历其中每个地址,逐条执行 forge verify-contract --constructor-args,单条失败不中断(|| true)。这解释了为什么代理合约与实现合约都需要各自可验证的源码与构造参数——GetDeployed 会为每个代理序列化 ERC1967Proxy 的构造参数 (实现地址, 初始化调用),为每个实现序列化各自的源码路径与参数(如 CometblsClient 实现的构造参数是 IBCHandler 地址,UCS03 则包含 SEND_IMPL/FAO_IMPL 等子实现)。

6. 部署元数据与链配置:deployments.json 和 evm.nix 的 networks

6.1 deployments.json 的结构

deployments/deployments.json 是各链部署地址的唯一数据源(协议文档 deployments 页面 的表格即由它渲染)。以 arbitrum.42161 为例,每个链条目包含:

{
  "ibc_interface": "ibc-solidity",
  "deployer": "0x6dd4e0224d46b60d86e57c9e5980589e9818020f",
  "sender": "0x95fb5cb304508d74d855514d7bc9bda75c304ce2",
  "contracts": {
    "0xee4ea8d358473f0fcebf0329feed95d56e8c04d7": {
      "name": "core",
      "salt": "0x6962632d69732d6261736564",
      "height": 420578864,
      "commit": "6fadb697bee413c025e87b5edf79c0d9f836cce6"
    }
  }
}

其中 salt 字段是 salt 字符串的十六进制编码(如 0x6962632d69732d6261736564 解码即 ibc-is-based),commit 记录部署时的源码版本,ibc_interface 区分 ibc-solidity(本仓库 EVM 侧)与其他 IBC 实现。同一份 salt + deployer + sender 在 Arbitrum 主网与 Sepolia 上产生了完全相同的合约地址(对比文件中 arbitrum.42161arbitrum.421614 两个条目即可验证),这正是第 2 节所述确定性地址机制在生产中的直接体现。

6.2 evm.nix 的 networks 表

evm/evm.nixnetworks 列表定义了每条链的:chain-id / universal chain id、RPC URL、部署私钥(测试网/主网通过 1Password op item get deployer ... 注入,devnet 则读取 networks/genesis/devnet-eth/ 下的密钥文件)、UCS03 参数(wethrate-limit-enabled、本币名称/符号/精度)、验证器配置(verifierverifier-urlverification-key)。例如 Base 主网启用了 UCS03 限流(rate-limit-enabled = "true")并指定 WETH 地址 0x4200...0006;而 devnet 的 verify 仅在 x86_64 系统启用(使用本地 Blockscout http://localhost/api)。该表是所有 eth-deploy-* / eth-verify-* / eth-upgrade-* / update-deployments-json-* / whitelist-relayers-* / set-bucket-config-* 命令的生成依据。

6.3 其他运维命令(补充)

  • update-deployments-json:遍历所有 ibc_interface == "ibc-solidity" 的链,通过 RPC 拉取部署高度并回写 deployments/deployments.json
  • whitelist-relayers-<chain>:对给定的每个中继者地址调用 Manager 的 grantRole(1, relayer, 0) 授予 RELAYER 角色;
  • set-bucket-config-<chain>:对 UCS03 调用 setBucketConfig(denom, capacity, refill_rate, false) 配置限流桶。

7. 小结与延伸阅读

  • 确定性:CREATE3 + (sender, salt) 双重隔离使合约地址可在部署前于任意链预计算,deployments.json 中 Arbitrum 主网/测试网地址一致的记录是机制的实证;
  • 可升级性:ERC1967Proxy 代理 + UUPS 实现升级,devnet 允许重部署(地址重算)、生产只升级(地址恒定);
  • 可审计性:固定编译配置(bytecode_hash = "none"cbor_metadata = false)+ 多浏览器验证命令,使任一链上合约都能对照指定 commit 复验。

如需继续深入,推荐阅读:evm/ARCHITECTURE.md(IBC 在 Solidity 中的分层设计、provableStore 承诺存储与轻客户端接口)、evm/scripts/Deploy.s.sol(全部部署/升级/查询脚本)、evm/evm.nix(链配置与命令生成)、evm/contracts/Manager.sol(UUPS + 访问控制中枢)以及 deployments/deployments.json(各链部署事实表)。

登录后查看全文
热门项目推荐
相关项目推荐