以太坊 CREATE2 操作码完全指南:如何在 Solidity 中预测合约地址

·

CREATE2 上线后,开发者终于可以在部署前就知道合约会落在哪个地址,这为跨链流动性桥、代理钱包、闪电贷工厂等场景打开了全新思路。本文将带你从底层原理到实战脚本,彻底掌握 CREATE2 地址预测Solidity 调用示例字节码-盐值暴力破解 三个关键技术点。

理解 CREATE 与 CREATE2 的区别

CREATE:默认却不可预测的部署方式

在普通 new Token() 的写法中,EVM 使用 CREATE

keccak256(rlp.encode(deployerAddress, nonce))[12:]

只要 nonce(同一地址发出过的交易总数)变化,结果地址便随之改变,因此无法提前锁定。链上查看时,我们得等“真正”部署完毕才能确认 Token 的地址,限制了某些业务提前调用的可能。

CREATE2:四段因子决定固定地址

CREATE2 引入额外 32 字节 salt,新地址由四部分拼接后哈希:

keccak256(0xff ++ deployerAddr ++ salt ++ keccak256(bytecode))[12:]

只要字节码、salt、部署者三者不变,提前计算的地址就是部署后的真实地址。这让它成为需要“可预测合约地址”的核心工具。

Solidity 0.8+ 的两种写法对比

过去只能用内联汇编,现在 合约层就能直接给 salt,对比一目了然:

旧写法(汇编)

function deploy(bytes32 salt) public {
    bytes memory bytecode = type(Token).creationCode;
    address addr;
    assembly {
        addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
    }
}

新写法(带 salt 参数)

function deploy(bytes32 salt) public returns (address) {
    Token t = new Token{salt: salt}();
    return address(t);
}
新写法自动帮你拼接 0xff。注意:盐值传错会导致部署失败(地址已被占用)或偏移。

案例:捕获 λ 开头的 Fuzzy Identity

目标:部署合约地址需包含字符串 badc0de,并且带函数 name() 返回 bytes32("smarx")。使用 Capture the Ether 的 Fuzzy Identity 挑战作例子。

第一步:编译获字节码

简易示例合约:

pragma solidity ^0.8.0;
interface IName { function name() external view returns (bytes32); }
contract BadCodeSmarx is IName {
    function name() external pure override returns (bytes32) {
        return bytes32("smarx");
    }
    function authenticate(address ctf) external {
        (bool success,) = ctf.call(abi.encodeWithSignature("authenticate()"));
        require(success, "AUTH_FAIL");
    }
}

truffle compile 或 Remix 可立刻拿到 bytecode。保存成十六进制字符串备用。

第二步:部署器合约

contract Deployer {
    bytes public constant factoryBytecode = hex"60806...";
    
    function deploy(bytes32 salt) external returns (address addr) {
        bytes memory runtime = factoryBytecode;
        assembly {
            addr := create2(0, add(runtime, 0x20), mload(runtime), salt)
        }
    }
}

部署器本身应先在测试网落位,以便获知 deployerAddress 参与后续哈希。

第三步:Python 或 JS 脚本穷举 salt

仅用单一私钥,无法穷举,但可以遍历 salt 值。核心公式复原:

preimage = "0xff" + deployerAddress + salt + keccak256(bytecode)
address  = keccak256(preimage)[26:]   // 小端 20 字节

Node.js 示例:

const { keccak256 } = require("ethereumjs-util");

const DEPLOYER = "0xCa4DfD86a86c48...".toLowerCase();
const BYTECODE_HASH = keccak256(Buffer.from(bytecode, "hex")).toString("hex");

for (let i = 0; i < 1e15; i++) {
  const saltHex = i.toString(16).padStart(64, "0");
  const preimage = "0xff" + DEPLOYER + saltHex + BYTECODE_HASH;
  const hash = keccak256(Buffer.from(preimage, "hex"));
  const addr = hash.subarray(12).toString("hex");
  if (addr.includes("badc0de")) {
    console.log("Salt:", saltHex);
    break;
  }
}

运行几十秒就输出:

Salt: 0x00000000000000000000000000000000000000000000000000000000005b2bfe

在部署器中调用 deploy(salt),即可把合约落在
0xa905a3922a4ebfbc7d257CECDB1dF04a3badc0de,随后通关。

👉 点开发现更多关于 CREATE2 在闪电贷聚合金库中的应用案例!

进阶:在多链场景下的实战技巧

  1. 钱包恢复:用户 Create2DeterministicWallet,在 ETH、BSC、Arbitrum 等链都复用相同地址,避免私钥泄露。
  2. 跨链桥流动性:部署相同字节码得到同地址跨链池子,提高用户直觉与资金整合。
  3. 批量空投预热:提前告知用户合约地址,允许他们先向地址存 ETH,降低官方托管成本。

👉 手把手教你用工厂合约在不同网络无痛同步地址信息

FAQ:关于 CREATE2 你最关心的五个问题

Q1:同一代码、同一 salt、不同 deployer 会得到相同地址吗?
不会。部署者地址是哈希输入的一部分;更换钱包或工厂合约都会改变结果。

Q2:CREATE2 部署失败会回滚吗?会!一旦目标地址已被其他合约占用,create2 返回 0,强烈检查返回值。

Q3:可以用 0 盐吗?可以。0x000...000 是合法的 32 字节 salt,只要不与现有部署撞车即可。

Q4:合约地址能携带 EIP-1167 最小代理吗?能。只要字节码与盐值锁死,代理克隆出来的子地址仍具有可预测性。

Q5:是否存在安全陷阱?极端值为 2^256 的 salt 并不影响安全;真正风险在于 重复 salt字节码变更 导致地址变动,记得每次固化字节码后使用新的 “发布版本号” 做 salt 前缀。