WTF Solidity 极简入门:CREATE2 操作码原理与极简 Uniswap 实战
【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
CREATE2 是 Solidity 中最具实用价值的操作码之一:它允许开发者在合约部署到链上之前就精确预测目标合约的地址,这是 Uniswap V2 创建 Pair 合约、交易所为用户预留合约钱包地址以及众多 Layer2 项目的基础能力。本文以 WTF-Solidity 仓库第 25 讲的葡萄牙语文档 Languages/pt-br/25_Create2/readme.md 为主体,结合仓库源码与测试用例,系统讲解 CREATE2 的地址计算原理、使用语法,并手把手实现一个可提前计算币对地址的极简 Uniswap。
为什么需要预测合约地址
在链上业务中,合约之间经常需要互相引用地址。如果新合约的地址只能在部署完成后通过返回值获取,调用方就必须依赖「先部署、再注册」的多步流程,增加一次跨合约调用,也让合约的组合变得脆弱。CREATE2 解决了这一问题:只要创建者、salt 与合约字节码确定,新合约的地址就唯一确定,任何人都可以在链下或链上提前算出它。
一、CREATE 如何计算地址(与 CREATE2 的对比)
智能合约可以由其他合约或普通账户(EOA)通过 CREATE 操作码创建。无论哪种方式,新合约地址都按同一公式计算——创建者地址与 nonce 的哈希:
新地址 = hash(创建者地址, nonce)
其中:
- 创建者地址:通常指部署钱包地址或创建合约的地址;
- nonce:对于 EOA 是该地址发出的交易总数;对于合约账户是它创建的合约总数,每创建一个合约 nonce +1。
创建者地址不会变化,但 nonce 会随时间改变,因此用 CREATE 创建的合约地址很难提前预测——这正是 CREATE2 要解决的问题:让合约地址独立于未来发生的任何事件,无论链上后续发生什么,都能把合约部署到事先算好的地址上。
二、CREATE2 的地址计算:四个决定要素
用 CREATE2 创建的合约地址由四部分共同决定:
| 0xFF | 一个固定常数,用于避免与 CREATE 地址计算产生冲突 |
| CreatorAddress | 调用 CREATE2 的当前合约(创建合约)的地址 |
| salt(盐) | 创建者指定的 bytes32 值,主要作用是影响新合约地址 |
| initcode | 新合约的初始字节码(合约的 Creation Code 与构造函数参数) |
新地址 = hash("0xFF", 创建者地址, salt, initcode)
CREATE2 保证了:只要创建者使用给定的 salt 部署给定的 initcode,该合约就一定被存储在上述公式算出的 新地址 上。四个参数中 salt 是开发者的「自由度」所在——通过改变 salt,可以在同一个创建合约中反复生成不同的确定性地址。
三、CREATE2 的语法与用法
CREATE2 的用法与第 24 讲介绍的 CREATE 几乎一致,同样是 new 一个合约并传入构造函数所需参数,只是必须多传一个 salt 参数:
Contract x = new Contract{salt: _salt, value: _value}(params)
参数含义:
- Contract:要创建的合约名;
- x:合约对象(即新合约地址);
- _salt:指定的盐值,类型为 bytes32;
- _value:可选,若构造函数为 payable,可在创建时转入 _value 数量的 ETH;
- params:新合约构造函数的参数。
四、实战:用 CREATE2 实现极简 Uniswap
与上一讲 24_Create 使用 CREATE 创建合约 类似,本讲用 CREATE2 实现极简版 Uniswap。完整代码位于仓库 25_Create2/Create2.sol(葡萄牙语带注释版本见 Languages/pt-br/25_Create2/Create2.sol),包含 Pair 与 PairFactory2 两个合约。
4.1 Pair 合约
contract Pair {
address public factory; // 工厂合约地址
address public token0; // 代币1
address public token1; // 代币2
constructor() payable {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
}
Pair 合约非常简单,包含 3 个状态变量:
- factory:记录部署它的工厂合约地址,构造函数执行时被赋值为 msg.sender;
- token0、token1:币对中的两种代币地址;
- initialize:部署后由工厂合约调用一次,将 token0、token1 更新为币对代币地址,并通过 require(msg.sender == factory) 做权限校验(UniswapV2: FORBIDDEN),防止他人篡改。
之所以把代币地址的初始化放到 initialize 而不是构造函数中,是为了让同一个 initcode(不含参数的 Creation Code)可以配合不同的 salt 批量生成合约——地址计算因此可以保持统一形态。
4.2 PairFactory2 工厂合约
contract PairFactory2 {
mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
address[] public allPairs; // 保存所有Pair地址
function createPair2(address tokenA, address tokenB) external returns (address pairAddr) {
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); // 避免tokenA和tokenB相同产生的冲突
// 用 tokenA 和 tokenB 地址计算 salt
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); // 将tokenA和tokenB按大小排序
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 用 create2 部署新合约
Pair pair = new Pair{salt: salt}();
// 调用新合约的 initialize 方法
pair.initialize(tokenA, tokenB);
// 更新地址 map
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
工厂合约有两个状态变量:
- getPair:两个代币地址到币对地址的映射,便于根据代币快速查找币对;
- allPairs:币对地址数组,保存全部已创建的币对地址。
createPair2 函数是整个流程的核心,分为四步:
salt 取自代币地址哈希的设计值得注意:同一组代币(不论顺序)必然得到相同的 salt 与相同的 Pair 地址,从机制上杜绝了同一个币对被重复创建出多个不同合约的情况。
4.3 提前计算 Pair 地址:calculateAddr
// 提前计算pair合约地址
function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); // 避免tokenA和tokenB相同产生的冲突
// 用 tokenA 和 tokenB 地址计算 salt
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); // 将tokenA和tokenB按大小排序
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 计算合约地址方法 hash()
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(type(Pair).creationCode)
)))));
}
calculateAddr 在 Solidity 中完整复现了 CREATE2 的地址计算,每一部分都与前文公式一一对应:
- bytes1(0xff) → 公式中的 0xFF 常数;
- address(this) → CreatorAddress,即当前工厂合约地址;
- salt → 与 createPair2 中相同的排序哈希逻辑,确保两函数结果一致;
- keccak256(type(Pair).creationCode) → 公式中的 initcode 部分。注意这里先对 Creation Code 做一次 keccak256 哈希,再参与外层哈希计算,这是 EVM 地址推导的标准做法。
type(Pair).creationCode 是 Solidity 0.8+ 提供的编译期常量,即 Pair 合约的创建字节码(不含构造函数参数)。最外层用 uint160 截取 20 字节得到地址。
通过该函数可以验证:事先计算的地址与实际创建出的地址是否一致,这正是 CREATE2 可预测性的直接证明。
4.4 构造函数带参数时的地址计算
上面的 Pair 构造函数没有参数,但如果创建时给构造函数传参,例如:
Pair pair = new Pair{salt: salt}(address(this));
则计算地址时,必须把构造函数参数与 Creation Code 一起打包后再哈希,即把 initcode 从 keccak256(type(Pair).creationCode) 替换为:
keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
对应的完整地址计算变为:
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
)))));
这是实践中最容易踩坑的地方:只要创建时传入了任何构造函数参数,就必须在 initcode 后追加 abi.encode(参数),否则计算出的地址与实际部署地址不一致。
五、在 Remix 上验证:以 WBNB / PEOPLE 为例
仓库文档给出了可直接用于验证的两组链上地址:
WBNB 地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC 链上的 PEOPLE 地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
在 Remix 中的验证步骤:
仓库还附带了一个基于 Hardhat 的自动化测试脚本 25_Create2/create2test.js,以代码形式固化了对这一结论的验证:脚本先部署 Pair 与 PairFactory2,随后用 WBNB/PEOPLE 地址调用 calculateAddr 得到 predictedAddress,再执行 createPair2 并读取 allPairs(0) 得到实际地址,最后通过断言 expect(createPair2Address).to.equal(predictedAddress) 校验两者完全相等。读者可以运行该测试脚本在本地复现「预测地址 == 实际地址」的完整链路。
六、CREATE2 的实际应用场景
交易所为用户预留钱包合约地址:在用户注册前即可预先计算好其钱包合约地址,用于充值与交互,无需等待链上部署完成。
由 CREATE2 驱动的 Factory 合约:Uniswap V2 的交易对创建正是在 Factory 中通过调用 CREATE2 完成的。这样做的好处是:由于 pair 地址是确定的,Router 可以直接用 (tokenA, tokenB) 在本地计算出 pair 地址,不再需要额外执行一次 Factory.getPair(tokenA, tokenB) 的跨合约调用,显著减少调用次数并简化路由逻辑——这正是本讲 PairFactory2 + calculateAddr 组合的设计蓝本。
总结
本讲围绕 CREATE2 操作码展开:先通过与 CREATE 的对比阐明地址计算差异(CREATE 依赖可变 nonce,CREATE2 依赖 0xFF、创建者地址、salt 与 initcode 四个确定要素);再给出 new Contract{salt: …} 的标准用法;随后以极简 Uniswap 为例,完整实现了 Pair、PairFactory2 与提前计算地址的 calculateAddr,并强调构造函数参数必须并入 initcode 打包的细节;最后介绍了其在交易所地址预留与 Uniswap V2 Factory 架构中的实际价值。能在部署前确定合约地址,是 Uniswap 这类组合型协议以及众多 Layer2 项目得以成立的基础能力,建议读者结合 25_Create2/Create2.sol 源码与 25_Create2/create2test.js 测试脚本动手实践一遍。
【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考






