[ERC2876] 入金時に識別子を付与して入金情報を管理しやすくする仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、コントラクトへの入金時に識別子を渡して、入金情報を管理しやすくする仕組みを提案している**ERC2876**についてまとめていきます！

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

https://eips.ethereum.org/EIPS/eip-2876

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

# 概要

**ERC2876**は、ETHのデポジット（入金）を効率的かつ安全に処理するためのコントラクトインターフェースを定義する提案です。
この規格では、デポジットの際に8バイトの識別子（ID）を利用して送金者を識別できる新しいアドレス形式も導入されています。
これにより、複数のユーザーからのETH入金を1つのコントラクトで管理できるようになり、複雑な鍵管理やホットウォレットの使用を避けることが可能になります。

**ERC2876**対応のウォレットアプリケーションは、この識別子を含めた形式で送金を行うことで、デポジット先が誰からの入金であるかを即座に判別できます。
これにより、従来の「**1ユーザー＝1アドレス**」モデルが不要になり、コスト削減にもつながります。

また、ETHの転送時にかかるガスコストを抑える効果もあり、通常の送金（`42000 gas`）よりも安価（約`30000 gas`）な処理が可能になる場合があります。
さらに、入金されたETHを即座にコールドウォレットへ自動転送できるよう設計されており、人的な操作を減らすことができます。

# 動機

中央集権型の取引所やECサイト（以下「アプリ」）は、ユーザーからのETH入金を受け付けるためのアドレスを管理する必要があります。
現在は、ユーザーごとに別々のアカウント（外部アカウントまたはコントラクトアカウント）を用意する方法が主流ですが、これには大きな問題があります。

- 外部アカウントを使う場合、ホットウォレットになってしまいセキュリティリスクが高まる。
- コールドウォレットを使う場合、入金のたびに各アカウントから資金を手動で回収する手間がかかる。
- コントラクトアカウントを使う場合、アカウント作成にかかるガスコスト（約`60000 gas`）が高すぎて現実的ではない。

そのため、アプリは「**セキュリティを犠牲にする**」か「**人件費をかける**」、「**高いガス代を払う**」の三択を迫られています。
特にネットワークのガス価格が高騰している時期には、新規参入のサービスほどホットウォレットに頼らざるを得ず、結果的にリスクが増大しています。

**ERC2876**は、これらの課題を解決するために、安価で安全に多数の入金を処理できる標準的な方法を提供しようとするものです。
この標準が広く採用されれば、世界中のサービスがアカウントを増やさずに効率よく資金のやりとりを行えるようになり、業界全体の運用コストとリスクの低減につながります。

# 仕様

## 用語

* **コントラクトインターフェース**
本ERCで定義されるスマートコントラクトの関数仕様を指します。

* **デポジットアドレス形式**
20バイトのアカウントアドレスと、8バイトのID（うち5バイトはnonce、3バイトはチェックサム）を合成した新しいアドレス形式です。

* **ID（8バイト）**
各入金を識別するための識別子で、5バイトのnonceと3バイトのチェックサムから構成されます。

* **親アプリケーション**
`deposit(bytes8)` 関数で受け取った情報を処理するアプリケーション（例：取引所やECサイトのバックエンド）。

* **デポジター（入金者）**
コントラクトにETHを送金するユーザー。

* **ウォレット**
デポジターの指示でトランザクションを作成・送信するアプリやライブラリ（例：MyEtherWallet、Ledger、blockchain.comなど）。

## デポジットアドレス形式

8バイトのIDを含めたアドレス形式を生成するために、以下の構造でアドレスを組み立てます。

```
28バイト = 20バイトのベースアドレス + 5バイトのnonce + 3バイトのチェックサム
```

チェックサムは、次の25バイト（20バイトのアドレス + 5バイトのnonce）をkeccak256でハッシュ化し、先頭3バイトを取り出したものです。

### JavaScriptによる生成コード例

```javascript
function generateAddress (baseAddress, nonce) {
  if (
    !baseAddress.match(/^0x[0-9a-fA-F]{40}$/) ||
    !nonce.match(/^0x[0-9a-fA-F]{10}$/)
  ) {
    throw new Error('Base Address and nonce must be 0x hex strings');
  }
  const ret =
    baseAddress.toLowerCase() + nonce.toLowerCase().replace(/^0x/, '');
  const myHash = web3.utils.keccak256(ret);
  return ret + myHash.slice(2, 8); // 先頭3バイトを追加
}
```

### Solidityによるチェックサムの検証例

```js
function checksumMatch(bytes8 id) internal view returns (bool) {
    bytes32 chkhash = keccak256(
        abi.encodePacked(address(this), bytes5(id))
    );
    bytes3 chkh = bytes3(chkhash);
    bytes3 chki = bytes3(bytes8(uint64(id) << 40));
    return chkh == chki;
}
```

## コントラクトインターフェース

**ERC2876**に準拠したコントラクトは以下の要件を満たす必要があります。

```js
interface DepositEIP {
  function deposit(bytes8 id) external payable returns (bool);
}
```

`msg.data` が空（つまり純粋なETH送金）の場合、トランザクションはrevertされます。
`deposit(bytes8)` には以下の3つの動作を実装する必要があります。

* **true を返す**
送金が完了し、親アプリでの処理が正常終了した場合。

* **false を返す**
ETHは保持するが、支払いがまだ完了していない場合（例：5ETH請求に対して3ETHのみ送金された）。

* **revert する**
入金処理が失敗し、ETHを保持しない場合。

* `deposit(bytes8)` はIDに含まれるチェックサムの検証を行うべきです。
* もし入金が一回限りの請求（請求額が定まっている）で、それを超える額が送られた場合、その超過分は `msg.sender` に即座に返金せず、アプリケーション側で管理・通知を行うべきである（SHOULD NOT直接返金）。

## ウォレットからの送金方法

ウォレットアプリやライブラリは以下の要件に従う必要があります。

* デポジットアドレス形式を、通常の20バイトアドレスの代替として扱えるようにする。
* 3バイトのチェックサムを検証し、一致しない場合は送金を拒否する。
* デポジットアドレス形式を使う場合、dataフィールドがnull（空）以外の値を持っていたら送金を失敗させること。
* 実際の送金トランザクションでは、以下のように各フィールドを設定する。
    * `to` フィールド
        * デポジットアドレス形式の最初の20バイト
    * `data` フィールド
        * 以下のような形式

    ```
    0x3ef8e69a + [8バイトID] + [24バイトゼロパディング]
    ```

例）

```
to:    0x433e064c42e87325fb6ffa9575a34862e0052f26
data:  0x3ef8e69a913fd924f056cd15000000000000000000000000000000000000000000
```

この形式により、ウォレットからの送金がスムーズに行われ、デポジット先コントラクトでも送金者の情報を正確に識別・処理できるようになります。

# テスト

```bash
[
  {
    "address": "0x083d6b05729c58289eb2d6d7c1bb1228d1e3f795",
    "nonce": "0xbdd769c69b",
    "depositAddress": "0x083d6b05729c58289eb2d6d7c1bb1228d1e3f795bdd769c69b3b97b9"
  },
  {
    "address": "0x433e064c42e87325fb6ffa9575a34862e0052f26",
    "nonce": "0x913fd924f0",
    "depositAddress": "0x433e064c42e87325fb6ffa9575a34862e0052f26913fd924f056cd15"
  },
  {
    "address": "0xbbc6597a834ef72570bfe5bb07030877c130e4be",
    "nonce": "0x2c8f5b3348",
    "depositAddress": "0xbbc6597a834ef72570bfe5bb07030877c130e4be2c8f5b3348023045"
  },
  {
    "address": "0x17627b07889cd22e9fae4c6abebb9a9ad0a904ee",
    "nonce": "0xe619dbb618",
    "depositAddress": "0x17627b07889cd22e9fae4c6abebb9a9ad0a904eee619dbb618732ef0"
  },
  {
    "address": "0x492cdf7701d3ebeaab63b4c7c0e66947c3d20247",
    "nonce": "0x6808043984",
    "depositAddress": "0x492cdf7701d3ebeaab63b4c7c0e66947c3d202476808043984183dbe"
  }
]
```

# 補足

**ERC2876**で定義されているコントラクトインターフェースとアドレス形式には、1つの制約があります。
それは、ETH（ネイティブ通貨）での入金にしか対応しておらず、**ERC20**などのトークンには対応していないという点です。

ただし、この点は大きな問題とは見なされていません。
なぜなら、実際の取引所やECサイトでは、通貨ごとにウォレットを論理的かつ鍵レベルで分離するのが会計上・セキュリティ上のベストプラクティスだからです。
したがって、ETHについてはこのERCを使い、**ERC20**トークンなどについては別の手法を使うという運用が推奨されます。

なお、**ERC20**で同様の仕組みを実現しようとすると、`transfer`関数などに新たな引数（ID情報）を追加する必要があり、ERC自体のスコープが大きくなりすぎてしまいます。
ただし、このアドレス形式が広く普及すれば、将来的に他のプロトコルにも `bytes8 id` を追加することは技術的には容易です（普及のハードルはあるものの）。

ID（8バイト）の構造については以下のような考慮のもとで設計されています。

* **チェックサム3バイト（24ビット）**は、**EIP55**で用いられる平均15ビットのチェックサムよりも高精度です。
* **nonce 5バイト（40ビット）**により、1兆以上の異なるIDが生成可能です。
* **ID全体で8バイト（64ビット）**に抑えることで、ハッシュ処理にかかるガスコストが安価になります（256ビット未満の方が効率が良いため）。

:::note info
**EIP55**については以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/985f03760df12929b5f9

# 互換性

**ERC2876**で定義される「**デポジットアドレス形式**」は、従来の20バイトのEthereumアドレスとは異なるため、対応していないウォレットやアプリケーションでは無効なアドレスとみなされます。

ただし、技術に詳しいユーザーであれば、以下の方法で手動対応することも可能です。

* チェックサムを自分で検証する
* `msg.data` を自作し、ウォレットアプリのUIに手動入力する（任意のdataフィールド入力が可能なウォレットであれば対応可能）

このため、例えばGitHub上に専用のツールを公開し、ユーザーが「**デポジットアドレス**」から20バイトのアドレスと適切な `msg.data` を得られるようにすることも考えられます。

また、**ERC2876**に準拠するコントラクトは、純粋なETH送金（`msg.data`が空の取引）を拒否するように設計されています。
そのため、誤って単純な送金だけを実行しても、入金が失敗するようになっており、安全性が確保されています。

**ERC2876**は非常にシンプルで実装も容易なため、提案者はまず `web3.js` で対応を行い主要ウォレットアプリへの採用を働きかけていく予定です。

# セキュリティ

## コールドウォレットへの自動転送

入金されたETHは、できる限り即座にコールドウォレットへ転送されるべきです。
これにより、万が一コントラクトが攻撃された場合でも、資金がコントラクト内に残っているリスクを最小限に抑えることができます。

このコールドウォレットのアドレスは、Solidityの以下いずれかの方法で変更できない値として定義する必要があります。

* `constant` キーワードを使用する
* Solidity v0.6.5以降で利用可能な `immutable` キーワードを使用する

これにより、デプロイ後にアドレスが変更されることを防ぎ、悪意ある書き換えを抑止できます。

## 親アプリケーションの停止に対する対策

取引所やECサイトなどの「**親アプリケーション**」が将来的に停止した場合、そのままコントラクトが動き続けると、ユーザーが誤って送金してしまうリスクがあります。

このような事態を防ぐために、コントラクトには**「**キルスイッチ**」機能を実装する必要があります。
これは、`deposit(bytes8)` 関数へのすべての呼び出しをrevert（失敗）させる機能であり、コントラクトの自爆（`selfdestruct`）ではありません。

`selfdestruct` を使うとコントラクトのコード自体は消えますが、アドレスは依然として有効な外部アカウントとして機能するため、ユーザーがこのアドレスにETHを送ると資金が回収不能になる「**ブラックホール**」と化してしまいます。

そのため、安全な停止方法としては、`deposit(bytes8)` の実行をブロックする制御ロジックを組み込むのが最も望ましい設計です。

# 引用

Jonathan Underwood (@junderw), "ERC-2876: Deposit contract and address standard [DRAFT]," Ethereum Improvement Proposals, no. 2876, August 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2876.

# 最後に

今回は「コントラクトへの入金時に識別子を渡して、入金情報を管理しやすくする仕組みを提案している**ERC2876**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4