[ERC7615] コントラクト間でアトミックにデータをプッシュする仕組みを理解しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、**パブリッシャーコントラクト**と**サブスクライバーコントラクト**の2つのコントラクト間でデータをプッシュして、オンチェーンでデータ更新通知を受け取る仕組みを提案している**ERC7615**についてまとめていきます！

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

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

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

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

# 概要

この規格では、Ethereumのスマートコントラクトでデータをプッシュ（送信）する仕組みを提案し、パブリッシャーコントラクト（データの送信元）が特定のデータをサブスクライバーコントラクト（データの送信先）に自動でプッシュできる仕組みを提案しています。

以下の2つのコントラクトのインターフェースが定義されていて、パブリッシャーコントラクトでは呼び出された関数がサブスクライバーコントラクトに対応しているかチェックされます。
もし対応していれば、サブスクライバーコントラクトにデータをプッシュするという仕組みです。

- **パブリッシャーコントラクト**
    - データを送信する役割を持つコントラクト。
- **サブスクライバーコントラクト**
    - データを受信する役割を持つコントラクト。

# 動機

現在、多くの「**キーパー**」(データを監視する役割を持つエンティティ)は、オフチェーン（ブロックチェーン外）のデータや別のデータ収集プロセスに依存して、オンチェーン（ブロックチェーン上）のイベントを監視しています。
この提案では、パブリッシャーコントラクトがサブスクライバーコントラクトに対してデータの更新を通知するシステムを確立することを目指しています。
これにより、直接オンチェーン上でパブリッシャーとサブスクライバー間でやり取りが行われ、システムがより信頼性高く効率的になります。

また、このシステムにより、DeFiアプリケーションが自由に拡張できるようになり、パブリッシャーが必要なデータを即座にサブスクライバーに伝えることで、より迅速かつ効率的な金融取引が可能になります。
他にも、分散型自律組織（DAO）のガバナンスプロセスが改善され、パブリッシャーが重要な更新情報を迅速にサブスクライバーに伝えることで、DAOメンバーが最新の情報に基づいて意思決定を行うことができます。
他にも以下のような場面でメリットがあります。

## Lending Protocol（レンディングプロトコル）
この例では、オラクルがパブリッシャーコントラクトとして機能します。
オラクルは市場の価格情報を提供する役割を持ちます。
オラクルが価格更新を行う時に、その情報をサブスクライバーコントラクトであるレンディングプロトコルに自動的に送信します。
レンディングプロトコルは、受け取った価格情報を基に自動的に貸し出しポジションの清算を行います。
これにより、迅速かつ効率的な価格反映とポジション管理が可能になります。

### Automatic Payment（自動支払い）
この例では、サービス提供者がパブリッシャーコントラクトとして機能します。
ユーザーがサービス提供者のコントラクトを呼び出すと、パブリッシャーコントラクトはサブスクライバーコントラクトに情報をプッシュ（送信）します。
サブスクライバーコントラクトとしては、ユーザーのウォレット（**ERC6551**標準のNFTバウンドアカウントやその他のスマートコントラクトウォレット）が該当します。
これにより、ユーザーのウォレットが自動的に支払い操作を実行します。
従来の承認が必要なアプローチに比べて、この方法では、例えば支払いの上限設定など、より複雑なロジックの実装が可能となります。

:::note info
**ERC6551**とは、**ERC721**形式のNFTの各`tokenId`に**TBA（Token Bound Acccount）と呼ばれるコントラクトを紐付ける規格です。
TBAはコントラクトであるため、他のNFTやFT、ネイティブトークンを受け取ることができます。
これにより、NFTが他のNFTやFT、ネイティブトークンを持つことができるようになります。
また、TBA内に保管されているNFTやFT、ネイティブトークンの操作権限は、TBAが紐付いているNFT（`tokenId`）の保有アドレスになります。
そのため、TBAが紐づているNFTが`transfer`されても、その操作権限はNFTを送付されたアドレスになります。
より詳しくは以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/8a0e50b939e0c95f3592

### PoS Without Transferring Assets（資産移動なしのPoS）
この例では、NFTステーキングのシナリオを対象としています。
PoS（プルーフ・オブ・ステーク）コントラクトがサブスクライバーとして機能し、NFTコントラクトがパブリッシャーとして機能します。
これにより、ユーザーは資産を`transfer`することなくステーキング報酬を得ることができます。
NFTの`transfer`が発生すると、NFTコントラクトがその情報をPoSコントラクトにプッシュします。PoSコントラクトは受け取った情報に基づいて、アンステーキングやその他の操作を実行します。

### DAO Voting（DAO投票）
この例では、DAOガバナンスコントラクトがパブリッシャーとして機能します。
投票が完了した後、自動的にプッシュメカニズムをトリガーし、関連するサブスクライバーコントラクトを呼び出して投票結果を直接実施します。
例えば、特定のアカウントやプールに資金を投入するなどの処理が自動的に行われます。
これにより、DAOのガバナンスプロセスが効率化され、迅速な実行が可能になります。

# 仕様

## 概要
プッシュメカニズムは以下の4つのステップに分けられます。

1. パブリッシャーコントラクトが呼び出される。
2. パブリッシャーコントラクトが呼び出された関数の`selector`からサブスクライバーリストを照会する。
   - サブスクライバーコントラクトが選択されたデータを`inbox`に格納する
3. パブリッシャーコントラクトがサブスクライバーコントラクトの`exec`関数を呼び出して、セレクタとデータをプッシュする。
4. サブスクライバーコントラクトがプッシュされたセレクタとデータに基づいて実行する、もしくは必要に応じてパブリッシャーコントラクトの`inbox`から情報を取得する。

2番目のステップであるサブスクライバーリストの照会で、パブリッシャーコントラクトは呼び出された関数のセレクタに基づいてサブスクライバーリストを照会します。
サブスクライバーコントラクトは、選択されたデータをinbox`に格納します。
呼び出された関数と対応するサブスクライバーの関係は、パブリッシャーコントラクト内で設定できま、設定方法には以下の2つがあります。
 - **無条件プッシュ**
     - 設定された`selector`を呼び出すとがプッシュが実行される。
 - **条件付きプッシュ**
     - 設定された`selector`の条件付き呼び出しによってプッシュが実行される。

また、パブリッシャーコントラクトは、サブスクライバーコントラクトの`exec`関数を呼び出し、セレクタとデータをプッシュします。
この時、単一の`selector`に対して複数の異なるタイプのサブスクライバーコントラクトを設定することが可能でき、パブリッシャーコントラクトは各サブスクライバーコントラクトの`exec`関数を呼び出してリクエストをプッシュします。
サブスクライバーコントラクトを`selector`から解除する時に、パブリッシャーコントラクトはサブスクライバーコントラクトの`isLocked`関数が`true`を返すか確認する必要があります。

4番目のステップでは、サブスクライバーコントラクトは`exec`関数の実装において、可能な限り`selector`リクエストとデータを処理する必要があります。
場合によっては、`exec`関数がパブリッシャーコントラクトの`inbox`を呼び出して、プッシュされたデータの全体を取得することがあります。

![ERC7615.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/73b28b94-225f-cb99-1577-a72f3a87af70.png)
引用: https://eips.ethereum.org/EIPS/eip-7615

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

無条件プッシュと条件付きプッシュの2つのタイプの実装タイプがあります。

### 

```js
interface IPushForce {
    event ForceApprove(bytes4 indexed selector, address indexed target);
    event ForceCancel(bytes4 indexed selector, address indexed target);
    event RenounceForceApprove();
    event RenounceForceCancel();

    error MustRenounce();
    error ForceApproveRenounced();
    error ForceCancelRenounced();

    function isForceApproved(bytes4 selector, address target) external returns (bool);
    function forceApprove(bytes4 selector, address target) external;
    function forceCancel(bytes4 selector, address target) external;
    function isRenounceForceApprove() external returns (bool);
    function isRenounceForceCancel() external returns (bool);
    function renounceForceApprove(bytes memory) external;
    function renounceForceCancel(bytes memory) external;
}
```

- `isForceApproved`
    - `selector`がターゲットサブスクライバーに無条件でバインドされているかどうかを確認します。
- `forceApprove`
    - `selector`をターゲットサブスクライバーにバインドします。
- `forceCancel`
    - `selector`とターゲットのバインディングを解除します。
    - この時、ターゲットの`isLocked`関数が`true`を返す必要があります。
- `renounceForceApprove`
    - `forceApprove`の権限を放棄します。
    - この関数を呼び出した後、``forceApprove`は呼び出せなくなります。
- `renounceForceCancel`
    - `forceCancel`の権限を放棄します。
    - この関数を呼び出した後、`forceCancel`は呼び出せなくなります。

### 条件付きプッシュ

```js
interface IPushFree {
    event Approve(bytes4 indexed selector, address indexed target, bytes data);
    event Cancel(bytes4 indexed selector, address indexed target, bytes data);

    function inbox(bytes4 selector) external returns (bytes memory);
    function isApproved(bytes4 selector, address target, bytes calldata data) external returns (bool);
    function approve(bytes4 selector, address target, bytes calldata data) external;
    function cancel(bytes4 selector, address target, bytes calldata data) external;
}
```

- `isApproved`
    - `selector`、ターゲット、データに基づいてプッシュが必要かどうかを確認します。
- `approve`
    - `selector`、ターゲット、データに基づいてバインドを承認します。
- `cancel`
    - `selector`、ターゲット、データに基づいてバインディングを解除します。
- `inbox`
    - サブスクライバーコントラクトから呼び出される場合のためにデータを格納します。

### パブリッシャーコントラクト

パブリッシャーコントラクトは以下の内部関数を実装する必要があります。

```js
function _push(bytes4 selector, bytes calldata data) internal;
```

この関数は、プッシュメカニズムを実装する必要があるすべての関数から呼び出されます。
この関数は、`selector`とデータに基づいて無条件および条件付きのサブスクリプションコントラクトを照会し、サブスクライバーの`exec`関数を呼び出します。

### サブスクライバーコントラクト

```js
interface IExec {
    function isLocked(bytes4 selector, bytes calldata data) external returns (bool);
    function exec(bytes4 selector, bytes calldata data) external;
}
```

`exec`関数は、パブリッシャーコントラクトからのリクエストを受け取って実行します。
`isLocked`は、`selector`とデータに基づいて、サブスクライバーコントラクトがパブリッシャーコントラクトのサブスクリプションを解除できるかどうかを確認します。
これは、解除リクエストが受け取られたときに実行されます。

# 補足

## 無条件および条件付きの設定
パブリッシャーコントラクトが呼び出された時にプッシュをトリガーし、ガス代をの支払いをリクエストすることができます。
レンディングプロトコルに価格変更をプッシュするなど無条件プッシュが必要な場合もあります。
一方で、条件付きプッシュは不要なガス消費を削減するのに役立ちます。

### アンサブスクライブ前の`isLocked`チェック
`forceCancel`や`cancel`の前に、パブリッシャーコントラクトはサブスクライバーコントラクトの`isLocked`関数を呼び出して、一方的な解除を避ける必要があります。
サブスクライバーコントラクトは、パブリッシャーコントラクトに大きく依存している場合があり、アンサブスクライブは重大な問題を引き起こす可能性があります。
したがって、サブスクライバーコントラクトは十分に考慮して`isLocked`関数を実装する必要があります。

:::note info
アンサブスクライブとは、サブスクライバーコントラクトがパブリッシャーコントラクトからのプッシュ通知を受け取らないようにすることを指します。
:::

### `inbox`メカニズム
特定のシナリオでは、パブリッシャーコントラクトは`selector`と共に重要なデータだけをサブスクライバーコントラクトにプッシュし、完全なデータは`inbox`内に保存される場合があります。
パブリッシャーコントラクトからのプッシュを受け取った時、サブスクライバーコントラクトはオプションで`inbox`を呼び出すことができます。
`inbox`メカニズムは、プッシュ情報を簡素化しつつ完全なデータの利用可能性を確保してガス消費を削減します。

### 関数セレクタをパラメータとして使用
関数セレクタを使用してサブスクライバーコントラクトのアドレスを取得することで、より詳細な設定が可能になります。
サブスクライバーコントラクトは、プッシュ情報に基づいてリクエスト元の特定の関数を持つことでプッシュ情報をより正確に処理できます。

### 権限放棄によるセキュリティ強化
`forceApprove`や`forceCancel`の権限は、`renounce`関数を使用して放棄することができます。
`renounceForceApprove`や`renounceForceCancel`が呼び出されると、登録されたプッシュターゲットを変更できなくなり、セキュリティが許可されます。

# 参考実装

```js
pragma solidity ^0.8.24;

import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IPushFree, IPushForce} from "./interfaces/IPush.sol";
import {IExec} from "./interfaces/IExec.sol";

contract Foo is IPushFree, IPushForce {
    using EnumerableSet for EnumerableSet.AddressSet;

    bool public override isRenounceForceApprove;
    bool public override isRenounceForceCancel;

    mapping(bytes4 selector => mapping(uint256 tokenId => EnumerableSet.AddressSet targets)) private _registry;
    mapping(bytes4 selector => EnumerableSet.AddressSet targets) private _registryOfAll;
    // mapping(bytes4 => bytes) public inbox;

    modifier notLock(bytes4 selector, address target, bytes memory data) {
        require(!IExec(target).isLocked(selector, data), "Foo: lock");
        _;
    }

    function inbox(bytes4 selector) public view returns (bytes memory data) {
        uint256 loadData;
        assembly {
            loadData := tload(selector)
        }

        data = abi.encode(loadData);
    }

    function isApproved(bytes4 selector, address target, bytes calldata data) external view override returns (bool) {
        uint256 tokenId = abi.decode(data, (uint256));
        return _registry[selector][tokenId].contains(target);
    }

    function isForceApproved(bytes4 selector, address target) external view override returns (bool) {
        return _registryOfAll[selector].contains(target);
    }

    function approve(bytes4 selector, address target, bytes calldata data) external override {
        uint256 tokenId = abi.decode(data, (uint256));
        _registry[selector][tokenId].add(target);
    }

    function cancel(bytes4 selector, address target, bytes calldata data)
        external
        override
        notLock(selector, target, data)
    {
        uint256 tokenId = abi.decode(data, (uint256));
        _registry[selector][tokenId].remove(target);
    }

    function forceApprove(bytes4 selector, address target) external override {
        if (isRenounceForceApprove) revert ForceApproveRenounced();
        _registryOfAll[selector].add(target);
    }

    function forceCancel(bytes4 selector, address target) external override notLock(selector, target, "") {
        if (isRenounceForceCancel) revert ForceCancelRenounced();
        _registryOfAll[selector].remove(target);
    }

    function renounceForceApprove(bytes memory data) external override {
        (bool burn) = abi.decode(data, (bool));
        if (burn != true) {
            revert MustRenounce();
        }

        isRenounceForceApprove = true;
        emit RenounceForceApprove();
    }

    function renounceForceCancel(bytes memory data) external override {
        (bool burn) = abi.decode(data, (bool));
        if (burn != true) {
            revert MustRenounce();
        }

        isRenounceForceCancel = true;
        emit RenounceForceCancel();
    }

    function send(uint256 message) external {
        _push(this.send.selector, message);
    }

    function _push(bytes4 selector, uint256 message) internal {
        assembly {
            tstore(selector, message)
        }

        address[] memory targets = _registry[selector][message].values();
        for (uint256 i = 0; i < targets.length; i++) {
            IExec(targets[i]).exec(selector, abi.encode(message));
        }

        targets = _registryOfAll[selector].values();
        for (uint256 i = 0; i < targets.length; i++) {
            IExec(targets[i]).exec(selector, abi.encode(message));
        }
    }
}

contract Bar is IExec {
    event Log(bytes4 indexed selector, bytes data, bytes inboxData);

    function isLocked(bytes4, bytes calldata) external pure override returns (bool) {
        return true;
    }

    function exec(bytes4 selector, bytes calldata data) external {
        bytes memory inboxData = IPushFree(msg.sender).inbox(selector);

        emit Log(selector, data, inboxData);
    }
}
```

# セキュリティ

### exec攻撃
`exec`関数は公開されているため、悪意のある呼び出しによって任意のプッシュ情報が挿入されるリスクがあります。
`exec`の実装では、`exec`呼び出し時に必ず検証する必要があります。

### リエントランシー攻撃
パブリッシャーコントラクトがサブスクライバーコントラクトの`exec`関数を呼び出すことにより、リエントランシー攻撃が発生する可能性があります。
悪意のあるサブスクライバーコントラクトが`exec`内でパブリッシャーコントラクトに対してリエントランシー攻撃を仕掛けることができるため、この点にも注意が必要です。

### 任意ターゲットの承認
`forceApprove`や`approve`の実装では、不必要なガス代の支払いを防ぐために、適切なアクセスコントロールが必要です。

### isLockedの実装
サブスクライバーコントラクトは、アンサブスクライブによる潜在的な損失を避けるために`isLocked`関数を実装する必要があります。
これは、特にレンディングプロトコルにとって重要です
。不適切なアンサブスクライブは異常な清算を引き起こして損失を招く可能性があります。

同様に、サブスクリプション時にパブリッシャーコントラクトは、`isLocked`が適切に実装されているかどうかを確認し、取取り消しできないサブスクリプションを防ぐ必要があります。

# 引用

Elaine Zhang (@lanyinzly) <lz8aj@virginia.edu>, Jerry <jerrymindflow@gmail.com>, Amandafanny <amandafanny200@gmail.com>, Shouhao Wong (@wangshouh) <wongshouhao@outlook.com>, Doris Che (@Cheyukj) <dorischeyy@gmail.com>, Henry Yuan (@onehumanbeing) <hy2878@nyu.edu>, "ERC-7615: Atomic Push-based Data Feed Among Contracts [DRAFT]," Ethereum Improvement Proposals, no. 7615, February 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7615.

# 最後に

今回は「**パブリッシャーコントラクト**と**サブスクライバーコントラクト**の2つのコントラクト間でデータをプッシュして、オンチェーンでデータ更新通知を受け取る仕組みを提案している**ERC7615**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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