Appearance
L2-7: CREATE2(決定論的デプロイ)
1. 課題
イーサリアム上でコントラクトをデプロイする際、コントラクトアドレスはデプロイヤーのアドレスとその nonce によって共同で決定されます(CREATE オペコード)。つまり、同じコントラクトコードでも異なるチェーンでは異なるアドレスになり、デプロイ前にアドレスを事前に知ることもできません。クロスチェーンブリッジ、L2 スケーリングソリューション、ERC-4337 スマートウォレットなど「複数チェーンで同一アドレス」を必要とするシナリオにとって、これは致命的です。
CREATE2 オペコードはこの問題を解決します。コントラクトアドレスが依存するのは、デプロイヤーアドレス、32 バイトの salt、コントラクト初期化コードのハッシュのみになります。この 3 つが変わらない限り、どのチェーンにデプロイしても、何回デプロイしても、得られるアドレスは同じです。さらに強力なのは、デプロイ前にアドレスを計算できることです。これにより「反事実的インスタンス化」が可能になります。つまり、コントラクトが実際に存在する前に、そのアドレスに資金を送ったり対話したりできるのです。
このチャレンジでは、任意の salt と initCode を使用した CREATE2 デプロイをサポートし、オンチェーンアドレス予測機能を提供する Create2Deployer コントラクト(src/level2/Create2Deployer.sol を参照)の実装が求められます。
2. 理由
CREATE2 はイーサリアムで最も重要なオペコードの 1 つです。L2 ブリッジコントラクト(Optimism や Arbitrum の L1 ブリッジなど)は、CREATE2 に依存して L1 と L2 に同じアドレスでミラーコントラクトをデプロイします。ERC-4337 アカウント抽象化の EntryPoint コントラクトは、すべての EVM チェーンで同じ CREATE2 アドレスを使用し、ウォレットの互換性を確保します。Uniswap V4 の Hook コントラクトも、決定論的デプロイに CREATE2 を活用しています。
CREATE2 のアドレス計算式を理解することは、イーサリアムの基盤となる EIP メカニズムを深く理解するための重要なステップです。アドレス計算式は単なる keccak256 演算ではありません。0xff プレフィックス、デプロイヤーアドレス、salt、initCodeHash の 4 つの要素の連結を含みます。0xff プレフィックスは、CREATE アドレスとの衝突を防ぎ、衝突攻撃を防止するために特別に設計されています。この計算式をマスターすれば、どのライブラリにも依存せずに、オンチェーンとオフチェーンの両方で同じアドレスを独立して計算でき、セキュリティ監査やプロトコル設計に不可欠です。
さらに、CREATE2 には重要な特性があります。あるアドレスのコントラクトが SELFDESTRUCT で破棄されても、同じ salt + initCode の組み合わせで同じアドレスに新しいコントラクトを再度デプロイできます。この「アドレス再利用」機能は強力なツールであると同時に、潜在的なセキュリティの落とし穴でもあります。既にデプロイされたコントラクトを上書きしないように、デプロイ前に address.code.length > 0 を明示的にチェックする必要があります。
3. 解決策
コアアーキテクチャ
Create2Deployer コントラクトは 3 層のインターフェースを提供します:
- デプロイ層(
deploy関数):インラインアセンブリを使用して CREATE2 オペコードを実行し、デプロイ前にアドレスが既に占有されているかチェックします - アドレス計算層(
computeAddress関数):標準の CREATE2 アドレス計算式を実装し、(salt, initCodeHash)と(salt, initCode)を受け取る 2 つのオーバーロードがあります - ユーティリティ層(
computeInitCodeHash、isDeployed):initCode ハッシュ計算とデプロイ状態クエリを提供します
アドレス計算式
address = keccak256(0xff + sender + salt + keccak256(initCode)) の末尾 20 バイト
ここで:
0xff:CREATE アドレスとの衝突を防ぐ 1 バイトの定数プレフィックスsender:デプロイヤーコントラクトのアドレス(20 バイト)salt:ユーザーが指定する 32 バイトの任意の値keccak256(initCode):初期化コードの keccak256 ハッシュ(32 バイト)
主要な実装詳細(src/level2/Create2Deployer.sol を参照)
solidity
// デプロイ関数 -- 最初に計算してからデプロイ
function deploy(bytes32 _salt, bytes calldata initCode) external returns (address deployedAddress) {
if (initCode.length == 0) revert EmptyInitCode();
// アドレスを事前計算し、既にデプロイされているかチェック
deployedAddress = computeAddress(_salt, computeInitCodeHash(initCode));
if (deployedAddress.code.length > 0) revert AlreadyDeployed();
// calldata から memory に initCode をコピー
bytes memory _initCode = initCode;
assembly {
// create2(value, offset, length, salt)
deployedAddress := create2(0, add(_initCode, 0x20), mload(_initCode), _salt)
}
if (deployedAddress == address(0)) revert DeploymentFailed();
emit Deployed(deployedAddress, _salt, msg.sender);
}
キーポイント:
add(_initCode, 0x20):Solidity の動的 bytes 配列の最初の 32 バイトの長さプレフィックスをスキップし、実際のバイトデータを直接指しますmload(_initCode):長さプレフィックスを読み取り、initCode のバイト数を取得しますcreate2は address(0) を返してデプロイ失敗を示します(例:ガス不足や initCode 実行の revert)
solidity
// アドレス計算 -- pure 関数、オンチェーンとオフチェーンの両方で呼び出し可能
function computeAddress(bytes32 _salt, bytes32 initCodeHash) public view returns (address addr) {
bytes32 hash = keccak256(
abi.encodePacked(bytes1(0xff), address(this), _salt, initCodeHash)
);
addr = address(uint160(uint256(hash)));
}
クロスチェーン同一アドレスデプロイ
異なるチェーンで同じアドレスを得るには、デプロイヤーコントラクト(Create2Deployer)自体が各チェーンで同じアドレスを持つ必要があります。これは 2 つの方法で実現できます:
- すべてのターゲットチェーンで同じデプロイヤーアドレスを使用する(Nick's Method で使用される
0x4e59b44847b379578588920cA78FbF26c0B4956Cシングルトンファクトリのような、決定論的なデプロイヤーアカウントが必要) - 元のトランザクションの nonce を通じて最初のファクトリアドレスの決定性を制御する
4. 遭遇した落とし穴
- デプロイ済みアドレスチェックの不十分さ:
deployedAddress.code.length > 0だけをチェックするのは不十分です。SELFDESTRUCT 後はcode.lengthが 0 になり、コントラクトが誤って上書きされます - initCode メモリレイアウトのエラー:アセンブリで
initCodeポインタを直接渡す際に、Solidity bytes の最初の 32 バイトが長さプレフィックスであることを忘れると、CREATE2 が誤ったデータを読み取ります - initCodeHash の混同:
keccak256(initCode)をコンストラクタ引数を含む最終バイトコードと混同し、計算されたアドレスが実際のデプロイアドレスと一致しなくなります - クロスチェーンガス制限の違い:異なるチェーンではガス制限が異なり、大きな initCode があるチェーンでは成功しても別のチェーンでは失敗し、「同一アドレス」の目標が達成できなくなります
- Metamorphic Contract のリスク:CREATE2 + SELFDESTRUCT を使用すると同じアドレスに異なるコードをデプロイでき、悪意のあるコントラクト「アップグレード」攻撃に悪用される可能性があります
5. 落とし穴の原因
デプロイ済みアドレスチェックは code.length に依存していますが、これは実行時の動的プロパティです。コントラクトが SELFDESTRUCT されると、コードは状態から削除されますがアドレスは依然として存在します。次回の CREATE2 で同じ salt + initCode を使用すると、同じアドレスに新しいコントラクトが生成されます。この「アドレス復活」機能は一部のシナリオ(Metamorphic Contract Factory など)で有用ですが、対策を講じないとユーザーの資金やロジックを誤って上書きする可能性があります。
initCode メモリレイアウトの問題は、Solidity の ABI エンコーディングに起因します。メモリ内では、bytes 型の最初の 32 バイトに長さが格納され、実際のバイトデータはオフセット 32 バイトから始まります。calldata では、ABI エンコーディング自体に 32 バイトのオフセットと長さフィールドがあります。assembly の create2 を使用する際は、ポインタ位置と長さを自分で制御する必要があります。add(_initCode, 0x20) は長さプレフィックスをスキップし、mload(_initCode) は長さの値を読み取ります。
initCodeHash の計算対象は、コントラクトの「作成バイトコード」(creation bytecode)でなければならず、「実行時バイトコード」(runtime bytecode)ではありません。作成バイトコードにはコンストラクタロジックと引数が含まれており、実行後に実行時バイトコードが生成されます。多くの人が Remix や Etherscan から実行時バイトコードをコピーしてアドレス計算に使用しますが、これは完全に誤った結果をもたらします。
6. 落とし穴の解決方法
デプロイ済みアドレスチェックの不十分さに対して:code.length > 0 のチェックに加えて、追加の上書き防止メカニズムを設けます。例えば、使用済みの salt をマッピングに記録したり、コントラクトが「アクティブ」かどうかを確認したりします(特定のストレージスロット値による判断など):
solidity
mapping(bytes32 => bool) public saltUsed;
function deploy(bytes32 _salt, bytes calldata initCode) external returns (address) {
// 二重チェック:コード長 + salt 使用記録
address predicted = computeAddress(_salt, computeInitCodeHash(initCode));
if (deployed.code.length > 0 || saltUsed[_salt]) revert AlreadyDeployed();
saltUsed[_salt] = true;
// ... デプロイロジック
}
initCode メモリレイアウトのエラーに対して:常に calldata bytes を memory 変数にコピーしてからアセンブリブロックに渡します。add(ptr, 0x20) で長さプレフィックスをスキップし、mload(ptr) で長さを取得します。アセンブリ内で calldata の bytes 変数を直接操作することは避けてください(Solidity 0.8 の calldata 変数はアセンブリ内で異なる表現方法を持ちます):
solidity
bytes memory _initCode = initCode; // memory にコピー
assembly {
addr := create2(0, add(_initCode, 0x20), mload(_initCode), _salt)
}
initCodeHash の混同に対して:コントラクト作成バイトコードの keccak256 ハッシュを使用することを確認します。Foundry では vm.getCode("ContractName") で作成バイトコードを取得できます。ethers.js では ethers.keccak256(contractFactory.bytecode) を使用します。常に computeInitCodeHash() ヘルパー関数を統一されたハッシュ計算のエントリポイントとして使用します。
クロスチェーンガス制限に対して:デプロイ前にターゲットチェーンのテストネットでガス消費量を検証します。initCode の設計時に過大なコンストラクタロジックを避けます。コンストラクタで immutable 変数を使用してストレージ書き込みの代わりにすることでデプロイガスを削減できます。
7. 技術的要点
| 要点 | 説明 |
|---|---|
| CREATE2 vs CREATE | CREATE2 アドレスは nonce に依存せず、(0xff, deployer, salt, initCodeHash) のみに依存 |
| アドレス計算式 | keccak256(0xff + deployer + salt + keccak256(initCode))[:20] |
| 0xff プレフィックス | CREATE アドレスとの衝突を防止し、アドレス空間内で 2 つのデプロイ方法を明確に区別 |
| 反事実的インスタンス化 | コントラクトのデプロイ前にアドレスを計算し、対話(ETH 送金、承認など)可能 |
| Metamorphic コントラクト | CREATE2 + SELFDESTRUCT = 同じアドレスに異なるコードをデプロイ可能、注意して使用 |
| Assembly オペコード | create2(value, offset, length, salt) -- offset は bytes の長さプレフィックスをスキップする必要あり |
| クロスチェーンデプロイ | deployer アドレス自体が各チェーンで同一である必要あり(決定論的デプロイヤーまたは nonce 制御による) |
| アドレス再利用 | SELFDESTRUCT 後のアドレスは CREATE2 で再利用可能、code.length チェックが必要 |