はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、複数のブロックチェーン上に同じアドレスでコントラクトをデプロイできる、CREATE2を使用したFactoryコントラクトの仕組みを提案しているERC2470についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

DApps(分散型アプリケーション)の中には、どのブロックチェーン上でも同一のアドレスで動作するコントラクト(シングルトン)を必要とするケースがあります。 例えば、ERC1820やERC2429のように、複数のプロジェクトが共通のインターフェースやレジストリとして参照するグローバルな基盤コントラクトにおいて特に重要です。 これらコントラクトは、アドレスが固定されていることが前提となっており、各チェーンで同じアドレスでデプロイされる必要があります。

ERC1820については以下の記事を参考にしてください。

ERC2470では、任意のチェーン上で誰でも同一のアドレスにコントラクトをデプロイできるようにするパーミッションレスなCREATE2ファクトリーの標準化を提案しています。 このファクトリーは、コントラクトのバイトコードとソルト(salt)に基づき、決定論的な(=同じ条件であれば常に同じ)アドレスにコントラクトをデプロイします。

CREATE2については以下を参考にしてください。

このファクトリーのデプロイには「Nick’s method」と呼ばれる手法が用いられており、一度限り使用される鍵の存在しないアカウントを使ってトランザクションを生成する方法です。 これにより、秘密鍵が存在しないことが保証されつつ、あらゆるチェーン上で同じアドレスにファクトリーを配置できます。

開発者はこのファクトリーを利用することで、ERC1820などのようにアドレスの一貫性が求められるコントラクトを、開発チェーンやテストネット、メインネットを問わず同じアドレスでデプロイすることができます。 これにより、開発環境の整合性が保たれ、コードの再利用性や相互運用性が向上します。

Nick’s method

通常のトランザクション署名と ecrecover

Ethereumでは、トランザクションに含まれるfromアドレスは直接指定されていません。 代わりに、署名(signature)とメッセージ(message)を入力としてecrecoverという関数を呼び出し、そこから公開鍵(public key)を復元し、それをハッシュ化してアドレスを算出します。

この仕組みにより、そのアドレスに対応する秘密鍵を持つ人だけが有効な署名を生成でき、ネットワークは「このトランザクションは正当な送信者によって作られた」と判断します。

署名を「意図的に」作らない場合

ここで通常の手順を無視し、署名部分(v, r, s)に好きな値を入れてみるとどうなるでしょうか。 ecrecoverは、与えられた署名値から必ず何らかの公開鍵を復元しますが、それは予測不可能なランダムに近いアドレスになります。

この場合、そのアドレスの秘密鍵は誰も知らないため、そのアドレスから追加でトランザクションを作ることはできません。 しかし、もしそのアドレスにEtherを送れば、あらかじめ用意した特定のトランザクションでのみ消費できる状態が作れます。

これにより、「一回限り使えるアドレス」(single-use address)を作ることが可能になります。 このアドレスは、そのトランザクションを実行するときにだけ使え、それ以降は使い道がありません。

一回限りのアドレスと資金移動

この手法では、以下のような流れになります。

  1. 任意の署名値(例:0xDA0DA0DA0... のような分かりやすいパターン)を指定してトランザクションを作成。
    • この値は予測可能なので、誰も秘密鍵を持たないことを第三者が検証可能です。
  2. このトランザクションの送信者アドレス(ecrecoverで計算されたアドレス)を取得。
  3. このアドレスにEtherを送付。
  4. そのEtherはあらかじめ作成された特定のトランザクションでのみ送金可能。

これにより、資金が特定の処理にのみ使われることを保証できます。

大規模送金(Multisend)への応用

記事の例では、extraBalance multisendという大量の送金処理にこの方法を応用しています。

まず、110件の送金先アドレスを持つトランザクションを大量(例:104件)に生成。 これらはすべて「一回限りアドレス」を送信者とするように作られています。 次に、これら104件のトランザクションにEtherを送るためのルートトランザクションを1つ作成。 このルートトランザクションも同じく一回限りアドレスを持ちます。

結果として、関係者(trustees)はこのルートトランザクションのアドレスにEtherを送るだけでよくなり、実際の送金は事前に検証済みのトランザクション群が順次実行され、最終的に全員へ送金されます。 必要な署名は最初の1回のみで、誰かが資金を流用するリスクがありません。

この方法の利点
  • 秘密鍵不要
    • デプロイや送金に使うアドレスの秘密鍵は誰も持たないため、安全性が高い。
  • 再現性
    • 署名値が決まっているため、誰でも同じ手順で同じアドレスを再計算可能。
  • 透明性
    • 全トランザクションは事前に公開・検証でき、正しい宛先・金額を誰でも確認可能。
  • 効率性
    • 大規模送金を1つの署名で実行可能。
具体例

Nick’s methodで使われている「一回限りのアドレス」がEVMレベルでどうやって成立しているか、ecrecover の動作を確認します。

1. 署名の基本構造

Ethereumの署名は以下の3つの値で構成されます。

  • v
    • リカバリID(公開鍵復元時のy座標の偶奇などを表す1byte)。
  • r
    • 署名の一部(楕円曲線暗号のx座標)。
  • s
    • 署名のもう一部(署名パラメータ)。

これらは楕円曲線署名(ECDSA)の結果で、通常は秘密鍵から計算されます。

2. ecrecover の処理フロー

Ethereumにおいてecrecoverは以下の処理を行います。

  1. 入力
    • メッセージハッシュ(hash)
    • 署名値(v, r, s)
  2. ECDSA復元 ECDSAの「公開鍵復元アルゴリズム」を使って、署名が作られたときの公開鍵を逆算します。 このときvが、どちらの曲線解(y座標)を使うかの判別に使われます。
  3. アドレス計算 復元した公開鍵をKeccak256でハッシュし、最後の20バイトを取り出してEthereumアドレスとします。
  4. 返り値 得られたアドレスを返します。

つまり、Ethereumのトランザクションにfromアドレスが明示的に入っていない理由は、このecrecoverを使って署名から送信者を導出するためです。

3. Nick’s methodでの応用

通常は有効な署名を作るために秘密鍵が必要ですが、Nick’s methodでは v, r, s に適当な値を入れます。

署名値がランダムなら、ecrecoverの結果はほぼランダムなアドレスになります。 このアドレスは偶然できたもので、誰も対応する秘密鍵を知りません。 しかし、そのアドレスはトランザクションの送信者としてEVMが受け入れるため、事前にETHを送ればトランザクションは有効に動作します。

4. 「半分の署名が有効」とは?

ECDSAの署名には「有効な数値範囲」があります。 r や s の範囲が楕円曲線の定義内で有効なら、ecrecoverは必ずアドレスを返します。 楕円曲線の性質上、ランダムな値を入れても約50%の確率で有効な署名として処理されるため、Nick’s methodはこの性質を利用します。

5. 実際の計算例

以下は簡略化した手順例です。

  • (1) 適当な署名値を選ぶ
text
v = 27
r = 0xDA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA
s = 0xDA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA0DA
  • (2) トランザクションのRLPエンコード部分をハッシュ化

EIP155を使わない場合は以下のように構築します。

text
hash = keccak256(rlpEncode(nonce, gasPrice, gasLimit, to, value, data, v, r, s))

ここでは署名値部分も含めて固定値でエンコード。

  • (3) ecrecoverで送信者アドレスを取得

EVM内部では以下が行われます。

text
pubkey = ECDSA_recover(hash, v, r, s)
address = keccak256(pubkey)[12:]  // 末尾20バイト

結果として得られるアドレスは、例えば以下のようになります。

text
0xBb6e024b9cFFACB947A71991E386681B1Cd1477D

これは秘密鍵の存在しない、完全に一時的な送信者アカウントです。

6. この仕組みを使った一回限りアドレスの流れ
  1. v, r, s に固定値を入れてトランザクションを構築。
  2. そのトランザクションの送信者アドレスをecrecoverで算出。
  3. そのアドレスにEtherを送付。
  4. トランザクションをネットワークにブロードキャストすると、 そのアドレスから資金が使われ、目的の処理が行われる。
  5. 秘密鍵がないため、このアドレスから別のトランザクションを送ることは不可能。

動機

ERC2470は、シングルトンコントラクトの再利用性を高めることを目的としています。 ファクトリーを利用することで、誰でも同じ方法でシングルトンコントラクトをデプロイできるようになり、コードの再利用やデプロイ作業の簡素化が可能になります。 これは複数のチェーンで同一アドレスを保証したいプロジェクトにとって有用です。

仕様

ERC2470 Singleton Factory と呼ばれるコントラクトについて提案されています。 このコントラクトは、CREATE2 命令(EIP1014)を利用して、事前に予測可能なアドレス(決定論的アドレス)にコントラクトをデプロイするための仕組みを提供します。

EIP1014については以下の記事を参考にしてください。

このファクトリー自体も「Nick’s Method」と呼ばれる特殊な手法によって、どのチェーンでも常に同じアドレスにデプロイされます。 この方法では、使い捨てのデプロイメントアカウントを使用し、その秘密鍵が存在しないことが保証されているため、セキュアかつ一貫性のある環境が実現されます。

インターフェース

SingletonFactory.sol
pragma solidity 0.6.2;


/**
 * @title Singleton Factory (EIP-2470)
 * @notice Exposes CREATE2 (EIP-1014) to deploy bytecode on deterministic addresses based on initialization code and salt.
 * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH)
 */
contract SingletonFactory {
    /**
     * @notice Deploys `_initCode` using `_salt` for defining the deterministic address.
     * @param _initCode Initialization code.
     * @param _salt Arbitrary value to modify resulting address.
     * @return createdContract Created contract address.
     */
    function deploy(bytes memory _initCode, bytes32 _salt)
        public
        returns (address payable createdContract)
    {
        assembly {
            createdContract := create2(0, add(_initCode, 0x20), mload(_initCode), _salt)
        }
    }
}
// IV is a value changed to generate the vanity address.
// IV: 6583047

デプロイ済みファクトリーの情報

  • 共通アドレス(全チェーンで同じ)

    text
    0xce0042B868300000d44A59004Da54A005ffdcf9f
  • デプロイに使用された単発アカウント(秘密鍵なし)

    text
    0xBb6e024b9cFFACB947A71991E386681B1Cd1477D
  • 必要送金額:0.0247 ETH(ガス代と安全マージンを含む)

  • デプロイメントトランザクションハッシュ

    text
    0x803351deb6d745e91545a6a3e1c0ea3e9a6a02a1a4193b70edfcd2f40f71a01c

deploy

javascript
function deploy(bytes memory _initCode, bytes32 _salt)
    public
    returns (address payable createdContract)

任意の初期化コードを使って、決定論的アドレスにコントラクトをデプロイする関数。 この関数は、_initCode と _salt を用いて CREATE2 命令を内部で呼び出し、コントラクトをデプロイします。 アドレスはこれらの値とファクトリーのアドレスに基づいて計算されるため、同じ値を使えばどのチェーンでも同じアドレスにデプロイされます。

引数

  • _initCode
    • デプロイ対象となるコントラクトの初期化コード。
  • _salt
    • デプロイ先アドレスを固定化するための32バイトの任意値。

戻り値

  • createdContract
    • デプロイされたコントラクトのアドレス。

SingletonFactory

javascript
contract SingletonFactory {
    function deploy(bytes memory _initCode, bytes32 _salt)
        public
        returns (address payable createdContract)
    {
        assembly {
            createdContract := create2(0, add(_initCode, 0x20), mload(_initCode), _salt)
        }
    }
}

このファクトリーを利用することで、EIP1820やEIP2429のような特定のアドレスに存在することが求められるコントラクトを、誰でも、どのネットワーク上でも同じアドレスでデプロイできます。 これはクロスチェーンDAppsや標準化されたインフラ構築において非常に重要な技術的基盤となります。

処理

「Keyless Deployment(Nick’s method)」を使って、秘密鍵を誰も知らない単発アカウントからコントラクトをデプロイし、どのチェーンでも同一アドレスでファクトリーを配置する手法を説明します。 トランザクションの署名値から送信者アドレスを逆算することで、秘密鍵に依存せずに一度きりのデプロイを実現します。

Keyless Deployment(Nick’s method)の考え方

通常のデプロイは「秘密鍵を持つEOA(外部所有アカウント)」が送信者になります。 Keyless Deploymentでは、署名パラメータ(v, r, s)に適当な値を与え、その値から送信者アドレスを逆算します。 逆算で得られる送信者は現実の秘密鍵に紐づかないため、誰も秘密鍵を知らない単発アカウント(single-use deployment account)として機能します。 このアカウントに必要額のETHを送り、用意済みのデプロイトランザクションを送信することで、コントラクトをデプロイできます。

なぜEIP155を使わないのか

EIP155は「チェーンIDを署名に含める規則」です。 チェーンIDとは、ブロックチェーンを一意に識別する番号のことです。 EIP155を使うと署名は特定チェーン専用になります。 今回の方法では、どのチェーンでも同じトランザクションとして検証されることが重要なため、EIP155をあえて使いません(legacy署名)。 これにより、任意のチェーン上で同じ送信者と同じデプロイ結果を再現できます。

トランザクションの中身

このでは、以下の固定パラメータでトランザクションを作ります。

  • nonce: 0
    • この単発アカウントにおける最初のトランザクションであることを示します。
  • gasPrice: 100000000000(100 Gwei)
    • ガス価格(1ガスあたりの支払単価)です。
    • 高めに設定し、どのチェーンでもマイナー/バリデータに拾われやすくします。
  • gasLimit: 247000
    • 実行に許される最大ガス量です。
    • 実際に必要なガスのおよそ2倍にあたる「247000」にしてあり、将来のOPCODE価格の変動でOut of Gasになりにくくしています。
  • value: 0
    • デプロイ自体にETHを送付していません。
  • data: <長い16進数>
    • コントラクトのデプロイ用バイトコードです。
  • v: 27, r: 0x247000, s: 0x2470
    • 署名要素です。
    • 本来は秘密鍵で計算されますが、ここではあらかじめ決められた値を使います。
    • r と s は楕円曲線署名の数値ですが、ここでは「2470」ではじまる覚えやすい値が置かれています。
    • これらの値から、復元手続き(署名から送信者を求める処理)により送信者アドレスが一意に決まります。
単発デプロイアカウントの成立

上記トランザクションの v, r, s と本文から、送信者アドレス(=単発アカウント)を復元できます。 ここで重要なのは、この送信者アドレスは「署名値から逆算」された結果であり、対応する秘密鍵を誰も持っていない前提であることです。 したがって、このアカウントはデプロイのためだけに一度使われるアドレスになります。

提示された単発アカウントは0xBb6e024b9cFFACB947A71991E386681B1Cd1477Dです。

必要なETHと残余

このアカウントに 0.0247 ETHを送ります。 内訳は以下です。

  • ガス上限が 247000 で、ガス価格が 100 Gwei のため、最大消費は 0.0247 ETH です。
  • **実消費は上限より少ないので、約 0.01 ETH程度が余り、単発アドレスに永遠にロックされます(秘密鍵がないため取り出せません)。
  • これは将来のガス増加に備える保険のコストです。
ブロードキャストと結果

準備ができたら、上記のデプロイトランザクションをそのままブロードキャストします。 今回の例の結果トランザクションハッシュは以下です。

  • 0x803351deb6d745e91545a6a3e1c0ea3e9a6a02a1a4193b70edfcd2f40f71a01c

これにより、各チェーンで同一のコードが、同一のロジックでデプロイされます。

同一アドレスでの配置(チェーンをまたいだ再現性)

この方法の最大の利点は、どのチェーンでも同じコントラクトアドレスを再現できることです。 ここでのファクトリーは以下の固定アドレスになります。

  • ファクトリーコントラクトアドレス
    • 0xce0042B868300000d44A59004Da54A005ffdcf9f

デプロイが常に同じ手順・同じトランザクション構造で行われるため、チェーンが変わってもファクトリーのアドレスが変わりません。 これにより、クロスチェーンでのアドレス依存ロジックやドキュメント整備が簡潔になります。

実施手順(最小限の箇条書き)
  1. 記載のパラメータ(nonce:0, gasPrice:100 Gwei, gasLimit:247000, value:0, data:<bytecode>, v=27, r=0x247000, s=0x2470)でデプロイトランザクションを用意します。
  2. 送信者アドレスを復元して、単発デプロイアカウント(0xBb6e...477D)を確認します。
  3. 0.0247 ETH をその単発アカウントへ送付します。
  4. 用意済みのトランザクションをブロードキャストします。
  5. 各チェーンでファクトリーは常に 0xce0042...cf9f に配置されます。
注意点と設計上の意図
  • EIP155を使わないことで、同一署名がチェーン横断で成立します。
    • チェーン依存を避け、同一アドレス配置を保証するための設計です。
  • ス上限を実必要量の2倍(247000)にしているのは、将来のOPCODEコスト変動で失敗しないようにするためです。
    • 余剰ETHは回収不能ですが、安定再現性のためのコストと割り切ります。
  • r, s は人為的に選んだ値で、「秘密鍵に依存しない」署名値から送信者を逆算する点がこの提案のコアロジックです。
  • 単発アカウントの秘密鍵は誰も知らない想定です。
    • したがって、デプロイ後にこのアカウントを使い回すことはできません。

ABI

json
[
    {
        "constant": false,
        "inputs": [
            {
                "internalType": "bytes",
                "name": "_initCode",
                "type": "bytes"
            },
            {
                "internalType": "bytes32",
                "name": "_salt",
                "type": "bytes32"
            }
        ],
        "name": "deploy",
        "outputs": [
            {
                "internalType": "address payable",
                "name": "createdContract",
                "type": "address"
            }
        ],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

補足

SingletonFactoryは、create2 によるデプロイ時に value(ETHの送金額)を指定できないように設計されています。 これは、チェーンによって異なる残高状態が生成されるのを防ぐためです。 例えば、あるチェーンではコントラクトがETHを持っていて、別のチェーンでは持っていないといった差異が生じると、同一アドレスでも状態が異なることになります。

また、このファクトリーは任意の salt(ソルト)をユーザーが指定できるようになっており、アドレスの一部に任意の文字列(バニティアドレス)を含めるような用途も可能になっています。 特に指定がなければ、ソルトには bytes(0) を使用すれば問題ありません。

注意点として、このファクトリーからデプロイされるコントラクトのコンストラクタで msg.sender を使用してはいけません。 msg.sender はデプロイ元アドレスを示すため、すべてのコントラクトが同じファクトリーからデプロイされると、所有者や初期化ロジックが意図せず統一されてしまう可能性があります。 そのため、初期状態はすべてコンストラクタ引数として渡すか、明示的な初期化関数を使う必要があります。

なお、生成されるアドレスは以下の式によりオンチェーンで計算可能です。

text
address(keccak256(0xff ++ factory_address ++ salt ++ keccak256(init_code))[12:])

具体的には以下のようになります。

text
address(keccak256(bytes1(0xff), 0xce0042B868300000d44A59004Da54A005ffdcf9f, _salt, keccak256(_code)) << 96)

JavaScriptの場合は、ethereumjs-utilライブラリの generateAddress2 関数を使うことで同様の計算ができます。

セキュリティ

SingletonFactoryを利用してデプロイされるコントラクトの中には、すべてのチェーンでのデプロイが適さないものや、チェーンごとに異なるアドレスが求められるケースがあります。 そのような場合には、EIP1344を使用してコンストラクタ内でチェーンIDを確認し、適切な動作を行うように設計することで安全性を確保できます。

EIP1344については以下の記事を参考にしてください。

特に、ウォレットと関連するアカウントコントラクトは「ユーザーにとってのシングルトン」として機能しますが、それが正しくチェーンIDを認識して動作するようにするためには、EIP1191に従ってチェーンIDの指定を行うべきです。

EIP1191については以下の記事を参考にしてください。

繰り返しになりますが、ファクトリーによるデプロイではconstructorで msg.senderを使用してはいけません。 msg.senderを使用すると、ファクトリー自身がすべてのコントラクトの所有者または管理者になってしまい、意図せず管理者になるリスクがあります。 代わりに、所有者などの情報はコンストラクタの引数で明示的に渡すべきです。

引用

Ricardo Guilherme Schmidt (@3esmit), "ERC-2470: Singleton Factory [DRAFT]," Ethereum Improvement Proposals, no. 2470, January 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2470.

最後に

今回は「複数のブロックチェーン上に同じアドレスでコントラクトをデプロイできる、CREATE2を使用したFactoryコントラクトの仕組みを提案しているERC2470」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!