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 在闪电贷聚合金库中的应用案例!
进阶:在多链场景下的实战技巧
- 钱包恢复:用户 Create2DeterministicWallet,在 ETH、BSC、Arbitrum 等链都复用相同地址,避免私钥泄露。
- 跨链桥流动性:部署相同字节码得到同地址跨链池子,提高用户直觉与资金整合。
- 批量空投预热:提前告知用户合约地址,允许他们先向地址存 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 前缀。