Skip to content
On this page

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 newnew{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 アドレス計算:

  • CREATEaddress = keccak256(sender, nonce)[12:] — 送信者の nonce に依存
  • CREATE2address = 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 + saltnew Contract{salt: salt}(args) → CREATE2 決定的デプロイ
Canonical Orderingtoken0 < token1 でトークン順序の曖昧さを排除
CREATE2 アドレス式address = keccak256(0xff, factory, salt, initCodeHash)[12:]
アドレスの事前計算デプロイ前にコントラクトアドレスを知ることができる
重複防止getPair[token0][token1] ルックアップ + CREATE2 の自然な衝突耐性
Uniswap V2このチャレンジの直接のインスピレーション(Pair + Factory アーキテクチャ)

Built with AiAda