CREATE3.sol 在 CREATE2 确定性地址的基础上,通过插入一个固定字节码的中间代理合约,将 initcode 从地址计算公式中剔除,实现部署地址仅由 deployer + salt 决定,彻底解耦合约地址与构造函数参数的绑定关系。
版本说明:[solmate]:main分支 [commit: 89365b8],[forge-std]:v1.15.0
源码链接:https://github.com/RevelationOfTuring/solmate/blob/main/src/utils/CREATE3.sol
CREATE3 是 solmate 的工具库,实现与 initcode 无关的确定性地址部署。整个库只有 3 个函数(deploy + 两个 getDeployed 重载),核心依赖一个 16 字节的内联代理合约(proxy)和 Bytes32AddressLib 工具库。
解决的核心问题:
CREATE 地址 = f(deployer, nonce) ← nonce 不可控,地址不确定
CREATE2 地址 = f(deployer, salt, keccak256(initcode)) ← initcode 包含构造函数参数,参数变则地址变
CREATE3 地址 = f(deployer, salt) ← 与 initcode 完全无关 ✅
CREATE2 看似解决了确定性地址问题,但它的地址公式包含 keccak256(initcode)。如果合约的构造函数参数变了(例如不同链上的 admin 地址不同),initcode 就变了,部署地址也随之改变。CREATE3 通过两步部署彻底消除了 initcode 对地址的影响。
依赖关系:
CREATE3.sol
└── Bytes32AddressLib.sol ← 提供 .fromLast20Bytes(),用于 keccak256 → address 转换
| 适合 | 不适合 |
|---|---|
| 跨链部署同一合约到相同地址(不同链上构造函数参数不同) | 只在单链部署且参数固定(CREATE2 足够) |
| 工厂合约需要提前承诺部署地址,但 initcode 尚未确定 | 部署频率极高、对 gas 极度敏感(CREATE3 多一次 CREATE2 + CALL 开销) |
| 协议升级时新旧版本需要部署到可预测地址 | 需要在同一 salt 下重复部署(CREATE3 每个 salt 只能用一次) |
| 需要在部署前将地址写入其他合约的配置中 | 简单的一次性部署脚本 |
CREATE3 (library)
│
├── Dependencies
│ └── using Bytes32AddressLib for bytes32 ← .fromLast20Bytes() 方法链
│
├── Constants
│ ├── PROXY_BYTECODE : bytes (16 bytes) ← 代理合约的完整字节码(initcode + runtime)
│ └── PROXY_BYTECODE_HASH : bytes32 ← keccak256(PROXY_BYTECODE),用于地址预计算
│
└── Functions(3 个 internal 函数)
├── deploy(salt, creationCode, value) → address ← 部署合约
├── getDeployed(salt) → address ← 预测地址(当前合约为 creator)
└── getDeployed(salt, creator) → address ← 预测地址(指定任意 creator)
CREATE3 的精妙之处在于将部署拆成两步,利用一个字节码固定的中间代理隔离 initcode 对地址的影响:
步骤 1: CREATE2 部署 proxy
proxy_address = keccak256(0xFF ++ deployer ++ salt ++ keccak256(PROXY_BYTECODE))[12:32]
→ PROXY_BYTECODE 是常量 → proxy 地址只取决于 deployer + salt ✅
步骤 2: proxy 内部用 CREATE 部署最终合约
final_address = keccak256(RLP([proxy_address, nonce=1]))[12:32]
→ proxy 地址确定 + nonce 固定为 1 → 最终地址确定 ✅
结论:final_address = f(deployer, salt) —— 与 creationCode 完全无关
完整调用链:
deploy(salt, creationCode, value)
│
├── 1. CREATE2(PROXY_BYTECODE, salt)
│ → 部署 proxy 到确定地址
│ → proxy 的 runtime = 0x363d3d37363d34f0(8 字节)
│
├── 2. proxy.call{value}(creationCode)
│ → proxy runtime 执行:
│ CALLDATACOPY → 将 creationCode 拷贝到内存
│ CREATE(value, 0, size) → 用 creationCode 部署最终合约
│
└── 3. 校验
→ require(proxy != address(0)) // CREATE2 成功
→ require(success && code.length != 0) // 最终合约部署成功
bytes internal constant PROXY_BYTECODE = hex"67_36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3";
这 16 字节是 CREATE3 的核心——一个完整的代理合约字节码(initcode + runtime),可能是以太坊上最小的功能性合约之一。
字节码布局:
hex"67 363d3d37363d34f0 3d 52 6008 6018 f3"
↑ ↑ ↑
│ └─ runtime(8 字节)—— 被 PUSH8 嵌入为立即数
└─ initcode 外壳(8 字节)
功能:将 runtime 字节码返回,使其成为 proxy 的 deployed code。
Opcode | Opcode + Args | Description | Stack
---------|------------------------|------------------|------------------
0x67 | 0x67XXXXXXXXXXXXXXXX | PUSH8 runtime | runtime
0x3d | 0x3d | RETURNDATASIZE | 0 runtime
0x52 | 0x52 | MSTORE |
| → MSTORE(0, runtime) |
| → 内存: [零 24 字节][runtime 8 字节] |
0x60 | 0x6008 | PUSH1 8 | 8
0x60 | 0x6018 | PUSH1 24 | 24 8
0xf3 | 0xf3 | RETURN |
| → RETURN(offset=24, size=8) |
| → 返回 runtime 作为 deployed code |
功能:将 calldata(= creationCode)原样传给 CREATE,部署为新合约。
Opcode | Opcode + Args | Description | Stack
---------|---------------|------------------|-----------------------
0x36 | 0x36 | CALLDATASIZE | size
0x3d | 0x3d | RETURNDATASIZE | 0 size
0x3d | 0x3d | RETURNDATASIZE | 0 0 size
0x37 | 0x37 | CALLDATACOPY |
| → CALLDATACOPY(destOffset=0, offset=0, size)
| → 将完整 calldata 拷贝到0x00起始的内存中
0x36 | 0x36 | CALLDATASIZE | size
0x3d | 0x3d | RETURNDATASIZE | 0 size
0x34 | 0x34 | CALLVALUE | value 0 size
0xf0 | 0xf0 | CREATE | newContract
| → CREATE(value, offset=0, size)
| → 用 calldata + msg.value 部署新合约
PUSH1 0x00 = 0x60 0x00 → 2 字节,3 gas
RETURNDATASIZE = 0x3d → 1 字节,2 gas
0x3d 的位置之前没有发生过外部调用,所以 RETURNDATASIZE 一定为 00x3d,总计节省 3 字节 + 3 gasEIP-3855 PUSH0 补充:Shanghai 升级引入了 PUSH0(0x5f),1 字节、2 gas,无条件压入 0,语义更清晰。但 Solmate 编写时 PUSH0 尚未引入,保留 RETURNDATASIZE 也确保了对所有 EVM 版本的向后兼容性(PUSH0 仅在 Shanghai 及之后的链上可用)。
bytes32 internal constant PROXY_BYTECODE_HASH = keccak256(PROXY_BYTECODE);
预计算的 proxy 字节码哈希,在 getDeployed() 中作为 CREATE2 地址公式的 bytecodeHash 参数。因为 PROXY_BYTECODE 是常量,所以 hash 也是常量。
function deploy(
bytes32 salt,
bytes memory creationCode,
uint256 value
) internal returns (address deployed) {
// 将 constant 字节码拷贝到 memory
// 原因:assembly 中的 create2 只能从 memory 读取数据,不能直接读 constant
bytes memory proxyChildBytecode = PROXY_BYTECODE;
address proxy;
/// @solidity memory-safe-assembly
assembly {
// 用 CREATE2 部署 proxy 合约
// create2(endowment, offset, size, salt):
// - 0: 不给 proxy 转 ETH(ETH 是给最终合约的)
// - add(proxyChildBytecode, 32): 跳过 bytes 的前 32 字节长度前缀,指向实际字节码
// - mload(proxyChildBytecode): 读取字节码长度(= 16 字节)
// - salt: 用户提供的盐值
// 返回值:部署成功返回 proxy 地址,失败返回 address(0)
proxy := create2(0, add(proxyChildBytecode, 32), mload(proxyChildBytecode), salt)
}
// proxy 地址为 0 说明 CREATE2 失败
// 典型场景:salt 已被使用过(该地址上已有 proxy 代码)
require(proxy != address(0), "DEPLOYMENT_FAILED");
// 预计算最终合约地址(纯数学推导,不依赖实际部署结果)
// 此时最终合约尚未部署,但地址已经确定
deployed = getDeployed(salt);
// 调用 proxy 合约,传入 creationCode 作为 calldata + 附带 ETH
// proxy 的 runtime 逻辑:
// 1. CALLDATACOPY → 将 creationCode 拷贝到内存
// 2. CREATE(value, 0, calldatasize) → 用 creationCode 部署最终合约,并转入 ETH
(bool success, ) = proxy.call{value: value}(creationCode);
// 双重校验:
// 校验 1: success = true → proxy.call 没有 revert
// 大多数情况都会返回 true,唯一返回 false 的场景:call 过程中 gas 不足
// 校验 2: deployed.code.length != 0 → 最终地址确实有合约代码
// 为什么需要?因为 proxy 的 runtime 不检查 CREATE 返回值:
// constructor revert → CREATE 返回 address(0) → 但 proxy 不关心,直接 STOP
// → proxy.call 仍然返回 success=true → 必须额外检查目标地址是否有代码
require(success && deployed.code.length != 0, "INITIALIZATION_FAILED");
}
作用:通过 CREATE3 模式部署合约,实现与 initcode 无关的确定性地址。内部先用 CREATE2 部署固定字节码的 proxy,再通过 proxy 用 CREATE 部署最终合约。
参数:
| 参数 | 类型 | 含义 |
|---|---|---|
salt |
bytes32 |
用户自定义的盐值,与 deployer 地址共同决定最终合约地址 |
creationCode |
bytes memory |
目标合约的完整创建字节码(包含构造函数参数) |
value |
uint256 |
部署时转给目标合约的 ETH 数量(wei) |
返回值:
| 类型 | 含义 |
|---|---|
address |
最终部署的合约地址 |
两种 revert 场景:
| revert 消息 | 触发条件 | 根因 |
|---|---|---|
"DEPLOYMENT_FAILED" |
proxy == address(0) |
CREATE2 失败:salt 已被使用(proxy 地址已有代码) |
"INITIALIZATION_FAILED" |
!success || deployed.code.length == 0 |
最终合约部署失败:constructor revert / 空 creationCode / gas 不足 |
function getDeployed(bytes32 salt) internal view returns (address) {
// 将 address(this) 作为 creator 参数
return getDeployed(salt, address(this));
}
作用:预计算 CREATE3 部署的最终合约地址,以当前合约(address(this))为 deployer。是双参数版本的便捷重载。
参数:
| 参数 | 类型 | 含义 |
|---|---|---|
salt |
bytes32 |
用户自定义的盐值 |
返回值:
| 类型 | 含义 |
|---|---|
address |
最终合约的预测地址 |
function getDeployed(
bytes32 salt,
address creator
) internal pure returns (address) {
// ═══════════════════════════════════════════════════════
// 第一步:用 CREATE2 公式计算 proxy 地址
// proxy = keccak256(0xFF ++ creator ++ salt ++ bytecodeHash)[12:32]
// ═══════════════════════════════════════════════════════
address proxy = keccak256(
abi.encodePacked(
bytes1(0xFF), // CREATE2 地址前缀(EIP-1014)
creator, // 部署者地址(20 字节)
salt, // 用户盐值(32 字节)
PROXY_BYTECODE_HASH // proxy 字节码哈希(32 字节,常量)
)
).fromLast20Bytes(); // 取 keccak256 结果的低 20 字节作为 proxy 地址
// ═══════════════════════════════════════════════════════
// 第二步:用 CREATE 公式(RLP 编码)计算最终合约地址
// deployed = keccak256(RLP([proxy, nonce=1]))[12:32]
// ═══════════════════════════════════════════════════════
return
keccak256(
abi.encodePacked(
// RLP 编码 [proxy, 1] 的完整字节序列:
// 0xd6 = 0xc0 + 22 → list 前缀(后续共 22 字节)
// 0x94 = 0x80 + 20 → string 前缀(后续 20 字节是 address)
hex"d6_94",
proxy, // 代理合约地址(20 字节)
hex"01" // nonce = 1(proxy 第一次执行 CREATE 时 nonce 为 1,EIP-161)
)
).fromLast20Bytes(); // 取低 20 字节 → 最终合约地址
}
作用:预计算 CREATE3 部署的最终合约地址,可指定任意 deployer。通过两步纯数学推导(CREATE2 公式 → RLP 编码)计算地址,不依赖任何链上状态。
参数:
| 参数 | 类型 | 含义 |
|---|---|---|
salt |
bytes32 |
用户自定义的盐值 |
creator |
address |
部署者地址(即调用 deploy() 的合约地址) |
返回值:
| 类型 | 含义 |
|---|---|
address |
最终合约的预测地址 |
RLP 编码结构图:
hex"d6_94" ++ proxy(20 bytes) ++ hex"01"
d6 ← list 前缀:0xc0 + 22 → list 前缀(后续共 22 字节)
├── 94 ← string 前缀:0x80 + 20 → string 前缀(后续 20 字节是 address)
│ └── <proxy address> ← 20 字节
└── 01 ← nonce = 1(单字节直接编码,RLP 规则:1~127 直接编码)
为什么 nonce 是 1?
0x01 是硬编码的常量,永远不会变 用户调用 deploy(salt, creationCode, value)
│
┌───────────────┼───────────────┐
│ │ │
▼ │ │
┌──────────────────────────┐ │ │
│ Step 1: CREATE2 │ │ │
│ 部署 proxy 合约 │ │ │
│ │ │ │
│ proxy_addr = CREATE2( │ │ │
│ value = 0, │ │ │
│ bytecode = PROXY_..., │ │ │
│ salt = salt │ │ │
│ ) │ │ │
└──────────┬───────────────┘ │ │
│ │ │
│ proxy_addr │ │
▼ │ │
┌──────────────────────────┐ │ │
│ require(proxy != 0) │ │ │
│ → "DEPLOYMENT_FAILED" │ │ │
└──────────┬───────────────┘ │ │
│ │ │
▼ │ │
┌──────────────────────────┐ │ │
│ deployed = getDeployed │ │ │
│ (salt) │ │ │
│ → 纯数学预计算 │ │ │
└──────────┬───────────────┘ │ │
│ │ │
▼ ▼ │
┌──────────────────────────────────────────┐ │
│ Step 2: proxy.call{value}(creationCode) │ │
│ │ │
│ proxy runtime 执行: │ │
│ 1. CALLDATACOPY → 拷贝 creationCode │ │
│ 2. CREATE(value, 0, size) │ │
│ → 部署最终合约到 deployed 地址 │ │
└──────────┬───────────────────────────────┘ │
│ │
│ success, deployed │
▼ │
┌──────────────────────────────────────────┐ │
│ require(success && │ │
│ deployed.code.length != 0) │ │
│ → "INITIALIZATION_FAILED" │ │
└──────────┬───────────────────────────────┘ │
│ │
▼ │
return deployed ◄──────────────────────────┘
deployer + salt
│
┌──────────────┴──────────────┐
▼ │
CREATE2 地址公式 │
keccak256( │
0xFF │
++ deployer │
++ salt │
++ keccak256(PROXY_BYTECODE) │
)[12:32] │
│ │
▼ │
proxy_address │
│ │
▼ │
CREATE 地址公式(RLP 编码) │
keccak256( │
0xd6 ← list 前缀 │
++ 0x94 ← string 前缀 │
++ proxy_address ← 20 字节 │
++ 0x01 ← nonce = 1 │
)[12:32] │
│ │
▼ │
final_address ═══════ f(deployer, salt) ← 与 creationCode 无关!
CREATE3 的核心洞察:在 deployer 和最终合约之间插入一个字节码固定的中间代理,将"可变的 initcode"从地址公式中隔离出去。 代理的字节码是常量,所以代理地址只取决于 deployer + salt;代理的 nonce 永远为 1,所以最终地址也只取决于代理地址。两层确定性叠加 = 最终地址的确定性。
16 字节实现了一个完整的合约部署器——包括 initcode 和 runtime 两个阶段。优化手段:
| 风险 | 说明 | 建议 |
|---|---|---|
| salt 一次性 | 每个 deployer + salt 组合只能部署一次(proxy 地址被占用后无法重复 CREATE2) | 设计 salt 生成策略时考虑唯一性(如 keccak256(abi.encode(name, version))) |
| constructor revert 静默 | proxy 不检查 CREATE 返回值,constructor revert 不会让 proxy.call revert | deploy 函数已通过 deployed.code.length != 0 校验兜底 |
| 无权限控制 | deploy 函数是 internal 的,任何继承合约都可以调用 |
在工厂合约中添加 onlyOwner 等权限修饰符 |
| 跨链地址一致性前提 | 同一 deployer 地址 + 同一 salt → 同一最终地址,但前提是 deployer 在各链上地址一致 | 确保工厂合约本身也是通过 CREATE2 跨链部署到相同地址 |
| ETH 转入不可逆 | value 参数直接传给 CREATE,如果 constructor revert,ETH 留在 proxy 中无法取回 | proxy 没有取回机制,实际由 deploy 函数的 require 保证失败时整个交易 revert |
| 方案 | 地址公式 | initcode 无关 | gas 开销 | 实现复杂度 |
|---|---|---|---|---|
| CREATE | f(deployer, nonce) |
✅(但 nonce 不可控) | 最低 | 最简单 |
| CREATE2 | f(deployer, salt, keccak256(initcode)) |
❌ | 低 | 简单 |
| CREATE3 (solmate) | f(deployer, salt) |
✅ | 中等(多一次 CREATE2 + CALL) | 中等 |
全部 Foundry 测试合约:https://github.com/RevelationOfTuring/foundry-solmate/blob/main/test/utils/CREATE3.t.sol
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
