Appearance
L2-21: Contract Factory(コントラクトファクトリーパターン)
1. 問題
コントラクトファクトリー(Factory)を実装するにはどうすればよいでしょうか?新しいコントラクトインスタンス(Uniswap の取引ペアコントラクトなど)をオンデマンドで作成し、作成アドレスの決定的な追跡を維持する必要があります。
具体的には:new 演算子 + salt を使用して ERC20 トークン取引ペアコントラクト(Pair)を作成する PairFactory コントラクトを実装します。決定的なアドレスの事前計算をサポートし、同じ取引ペアの重複作成を防ぎ、作成されたすべてのインスタンスを追跡する必要があります。
2. 理由
コントラクトファクトリーは DeFi インフラの中核パターンです。Uniswap、Aave、Compound などのプロトコルはすべて、多数の均質なコントラクトインスタンス(取引ペア、レンディングプールなど)を管理するためにファクトリーパターンを使用しています。
なぜファクトリーパターンが必要なのか?
- ガス効率:構造的に同一のコントラクトを手動で数百件もデプロイすることを回避(
new演算子で1回のデプロイで完了) - 決定的なアドレス:
saltメカニズムにより、同一のパラメータが常に同一のアドレスを生成。ユーザーはデプロイ前にアドレスを計算して信頼できる - 登録追跡:ファクトリーが作成されたすべてのコントラクトのインデックス(
allPairs配列)を維持し、フロントエンドのクエリを容易にする - 標準化:ファクトリーを通じて作成されたすべてのコントラクトは、同じ検証済みバイトコードを共有する
3. 解決策
アーキテクチャ設計
PairFactory
├── createPair(tokenA, tokenB)
│ ├── canonical ordering(小さい方のアドレスを先に)
│ ├── 重複防止: getPair[token0][token1] == 0
│ ├── salt = keccak256(token0, token1)
│ ├── new Pair{salt: salt}(token0, token1) ← CREATE2 デプロイ
│ ├── getPair[token0][token1] = pair
│ └── allPairs.push(pair)
│
├── computePairAddress(tokenA, tokenB) → address
│ └── CREATE2 アドレス式を手動実装して事前計算
│
├── getPair[token0][token1] → address
└── allPairs[] + allPairsLength()
Pair(子コントラクト)
├── token0: immutable address
├── token1: immutable address
├── factory: immutable address
└── getReserves() → (0, 0)
Canonical Ordering
solidity
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
このパターンは Uniswap V2 に由来します。(USDC, WETH) を渡しても (WETH, USDC) を渡しても、同じ取引ペアが作成されます。getPair[token0][token1] をルックアップする際に元の順序を気にする必要はありません。
CREATE2 アドレス事前計算
solidity
function computePairAddress(address tokenA, address tokenB) external view returns (address pair) {
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
bytes memory bytecode = abi.encodePacked(type(Pair).creationCode, abi.encode(token0, token1));
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode)));
pair = address(uint160(uint256(hash)));
}
CREATE2 アドレス式:address = keccak256(0xff || deployer || salt || initCodeHash)[12:]
4. 遭遇した落とし穴
4.1 new と new{salt} の違い
new Pair(token0, token1) → CREATE オペコードを使用。アドレスは (deployer, nonce) で決定。 new Pair{salt: salt}(token0, token1) → CREATE2 オペコードを使用。アドレスは (deployer, salt, initCode) で決定。
salt がない場合、各デプロイで異なるアドレスが生成されます。salt がある場合、同一パラメータ → 同一アドレス → 自然な重複デプロイ防止。
4.2 Canonical Ordering は両方の場所で同期させる必要がある
ファクトリーの createPair はソートするが Pair のコンストラクタはソートしない → Pair(token0, token1) の内部順序が一貫しない。 Pair のコンストラクタはソートするがファクトリーはソートしない → 同じトークンペアで2つの異なる Pair が作成される可能性がある。 Canonical ordering は両方の場所で適用する必要があります。
4.3 computePairAddress の init code 計算
solidity
// ✅ 正しい:コンストラクタ引数を含む
bytes memory bytecode = abi.encodePacked(type(Pair).creationCode, abi.encode(token0, token1));
// ❌ 間違い:引数なし。計算結果は実際のデプロイアドレスと一致しない
bytes memory bytecode = type(Pair).creationCode;
4.4 重複デプロイの二重防止
- アプリケーション層:
getPair[token0][token1] != address(0)チェック - EVM 層:
CREATE2はターゲットアドレスに既にコードがある場合 revert する
2層の保護により、取引ペアが2回作成されることは決してありません。
5. 落とし穴の原因
5.1
EVM アドレス計算:
CREATE:address = keccak256(sender, nonce)[12:]— 送信者の nonce に依存CREATE2:address = keccak256(0xff, sender, salt, initCodeHash)[12:]— 完全に決定的
5.2
canonical ordering が1箇所でしか適用されていない場合:
- ファクトリーでは適用、Pair では未適用:
createPair(A, B)→ Pair(token0=B, token1=A) → しかしユーザーは token0=A を期待するかもしれない - ファクトリーでは未適用、Pair では適用:
createPair(A, B)→ Pair(A, B);createPair(B, A)→ Pair(A, B) だが salt が異なる → アドレスが衝突しないため、同じトークンペアに対して2つのコントラクトが作成される可能性がある
6. 落とし穴の解決方法
solidity
function createPair(address tokenA, address tokenB) external returns (address pair) {
// 1. Canonical ordering
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB) : (tokenB, tokenA);
// 2. 重複チェック
require(getPair[token0][token1] == address(0), "Pair exists");
// 3. 決定的な salt でデプロイ
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
pair = address(new Pair{salt: salt}(token0, token1));
// 4. 登録
getPair[token0][token1] = pair;
allPairs.push(pair);
}
7. 技術ポイント
| ポイント | 説明 |
|---|---|
| ファクトリーパターン | 1つのコントラクトが複数の標準化された子コントラクトを作成・管理 |
new + salt | new Contract{salt: salt}(args) → CREATE2 決定的デプロイ |
| Canonical Ordering | token0 < token1 でトークン順序の曖昧さを排除 |
| CREATE2 アドレス式 | address = keccak256(0xff, factory, salt, initCodeHash)[12:] |
| アドレスの事前計算 | デプロイ前にコントラクトアドレスを知ることができる |
| 重複防止 | getPair[token0][token1] ルックアップ + CREATE2 の自然な衝突耐性 |
| Uniswap V2 | このチャレンジの直接のインスピレーション(Pair + Factory アーキテクチャ) |