[ERC7641] ERC20トークンに収益分配メカニズムを追加する仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC20**トークンを拡張して、収益分配メカニズムを統合して持続可能なトークンモデルの仕組みを提案している**ERC7641**についてまとめていきます！

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

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

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

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

# 概要

この規格では、**ERC20**トークンを拡張して、収益分配（Intrinsic RevShare）メカニズムを導入する提案をしています。

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

https://chaldene.net/erc20

**ERC20**との互換性を維持しつつ、トークン保有者間で収益を公平に分配するように設計されたインターフェースがy利義されています。
この提案の設計の中心となるのは、様々な場所からの収益を集約する収益プールの作成です。
トークン保有者はトークンを`burn`することで、収益プール内の自分の持分である「**シェア**」を取得することができます。
これにより、収益プールが空でない限りトークンの価値は常に収益プールの「**シェア**」に比例した価値を持ちます。
また、一定期間ごとにトークン保有者は収益プールから報酬をclaimすることができるため、所有者の参加が促されトークンの利用価値も向上します。

# 動機

## トークン保有者への収益分配

この提案では、定期的に報酬とclaimできる仕組みを導入して流動性を高め、トークン保有者に長期的な利益を提供する持続可能なトークンモデルの実現を目指しています。

**ERC20**の機能を継承することで、トークン保有者はトークンを売買しつつ、オプションの機能である`burn`メカニズムにより収益プールからの分配を受け取りながらトークンの供給量を減らすことができます。
また、持続可能な収益分配エコシステムにより、エコシステムへのトークン保有者の積極的な参加を促進します。

## あらゆるプロジェクトへの資金調達

この仕組みによりあらゆる種類のプロジェクトへのトークン化が可能になります。
トークン保有者が、オープンソースプロジェクトに積極的に貢献することで収益獲得のチャンスが増えます。
Initial Model Offering (IMO)というユースケースを導入し、オープンソースAIモデルの継続的な開発を行う資金調達の手段を提供できます。

:::note info
多くのオープンソースAIモデルdeは、貢献を収益化することに課題を抱えていました。
:::

また、オープンソースモデルを利用するプロジェクトは、収益プールに一定量の資金を支払うことでエコシステムの持続可能性に貢献できます。
Intrinsic RevShareトークン保有者は報酬を比例分配で受け取れるため、公正な分配メカニズムが確立されます。
ここで重要なのは、収益分配機能がトークン保有者への保証と指摘お主、長期的な収益メリットを促進してオープンソースAIコミュニティへの継続的な参加を促進している点です。

# 仕様

**IERC7641**に準拠するコントラクトは、以下のインターフェースを実装する必要があります。

```js
pragma solidity ^0.8.24;

/**
 * @dev An interface for ERC-7641, an ERC-20 extension that integrates a revenue-sharing mechanism, ensuring tokens intrinsically represent a share of a communal revenue pool
 */
interface IERC7641 is IERC20 {
    /**
     * @dev A function to calculate the amount of ETH claimable by a token holder at certain snapshot.
     * @param account The address of the token holder
     * @param snapshotId The snapshot id
     * @return The amount of revenue token claimable
     */
    function claimableRevenue(address account, uint256 snapshotId) external view returns (uint256);

    /**
     * @dev A function for token holder to claim ETH based on the token balance at certain snapshot.
     * @param snapshotId The snapshot id
     */
    function claim(uint256 snapshotId) external;

    /**
     * @dev A function to snapshot the token balance and the claimable revenue token balance
     * @return The snapshot id
     * @notice Should have `require` to avoid ddos attack
     */
    function snapshot() external returns (uint256);

    /**
     * @dev A function to calculate the amount of ETH redeemable by a token holder upon burn
     * @param amount The amount of token to burn
     * @return The amount of revenue ETH redeemable
     */
    function redeemableOnBurn(uint256 amount) external view returns (uint256);

    /**
     * @dev A function to burn tokens and redeem the corresponding amount of revenue token
     * @param amount The amount of token to burn
     */
    function burn(uint256 amount) external;
}
```

### claimableRevenue
特定のスナップショット時点でのトークン所有者が請求可能なETHの量を計算する関数。

- `account`
    - トークン所有者のアドレス。
- `snapshotId`
    - スナップショットのID。
- **戻り値**
    - 請求可能な収益トークンの量（ETH）。

### claim
特定のスナップショットに基づいて、トークン所有者がETHを請求する関数。
 - `snapshotId`
     - スナップショットのID。

### snapshot
トークン残高と請求可能な収益トークン残高をスナップショットとして記録する関数。
DDOS攻撃を防ぐために、適切な`require`文を使用する必要があります。

- **戻り値**
    - スナップショットのID。

### redeemableOnBurn
トークンを`burn`した時に、トークン所有者が引き換え可能なETHの量を計算する関数。
 - `amount`
     - `burn`するトークンの量。
- **戻り値**
    - 引き換え可能な収益ETHの量。

### burn
トークンを`burn`して収益分配トークンを受け取る関数。
 - `amount`
     - `burn`するトークンの量。


### 全体の流れ
1. **スナップショットの作成**
    - `snapshot`関数を呼び出してトークン残高と収益残高を記録。
2. **収益の請求**
    - `claim`関数を使って、特定のスナップショット時点の収益（ETH）を請求。
3. **burnと収益分配の受け取り**
    - `burn`関数を使ってトークンを`burn`して収益トークンを受け取る。

## AltRevToken（Option）

AltRevTokenは拡張機能であり、ETH以外の**ERC20**トークンも収益分配プールに入れることができるようにします。

```js
pragma solidity ^0.8.24;

/**
 * @dev An optional extension of the ERC-7641 standard that accepts other ERC-20 revenue tokens into the contract with corresponding claim function
 */
interface IERC7641AltRevToken is IERC7641 {
    /**
     * @dev A function to calculate the amount of ERC-20 claimable by a token holder at certain snapshot.
     * @param account The address of the token holder
     * @param snapshotId The snapshot id
     * @param token The address of the revenue token
     * @return The amount of revenue token claimable
     */
    function claimableERC20(address account, uint256 snapshotId, address token) external view returns (uint256);

    /**
     * @dev A function to calculate the amount of ERC-20 redeemable by a token holder upon burn
     * @param amount The amount of token to burn
     * @param token The address of the revenue token
     * @return The amount of revenue token redeemable
     */
    function redeemableERC20OnBurn(uint256 amount, address token) external view returns (uint256);
}
```

このコントラクトは、ERC-7641標準のオプション拡張であるAltRevTokenを定義しています。この拡張により、ETH以外の他のERC-20トークンも収益分配プールに受け入れることができます。


### claimableERC20

特定のスナップショット時点で、トークン所有者が請求可能な**ERC20**トークンの量を計算する関数。
 - `account`
     - トークン所有者のアドレス。
 - `snapshotId`
     - スナップショットのID。
 - `token`
     - 収益トークンのアドレス。
- **戻り値**
    - 請求可能な収益トークン（**ERC20**）の量。

### redeemableERC20OnBurn
トークンを`burn`した時に、トークン所有者が取得可能な**ERC20**トークンの量を計算する関数。
 - `amount`
     - `burn`するトークンの量。
 - `token`
     - 収益トークンのアドレス。
- **戻り値**
    - 引き換え可能な収益トークン（**ERC20**）の量。

# 補足

## 収益分配メカニズム

この規格では、トークン保有者が収益プールから比例分配を受け取れる収益分配メカニズムを実装しています。
定期的かつ透明性のある収益分配を行うために、トークン残高と各アドレスが取得できる収益トークン残高のスナップショットを定期的に保存しています。
各スナップショット中に、トークン保有者が受け取れるトークン量が分配メカニズムから導出されて請求できる権限が与えられます。

## snapshotインターフェース

トークン残高と請求可能な収益トークン残高を定期的にスナップショットとして記録するインターフェース。
これにより、各トークン保有者は自身の保有トークン量を正確に追跡し、透明性のある収益分配が実現します。
snapshotインターフェースでは、スナップショットに関連づいたトークン残高と請求可能な収益トークン残高にアクセスできる、一意の値`shanpshotId`を返します。

## claimableRevenueインターフェース

特定のスナップショットでトークン保有者が請求できるETHの量を計算するインターフェース。
収益は2つの連続するスナップショット間で分配します。
収益を2つのスナップショット間で分配します。
例えば、収益$R$があり、分配比率 $p$（0%-100%） に基づいて、収益の$pR$をトークン所有者に分配します。

請求可能なETHの量 =  $\frac{pR \times \text{amount}}{\text{totalAmount}}$
​
ここで、`amount`は特定の所有者のトークン数、`totalAmount`は全トークン数を表します。

:::note warn
残りの収益$(1-p)R$は収益プールに保持され、`burn`を通じて引き出すことができます。
:::

## claimインターフェース

特定のスナップショットでのトークン残高に基づいて、トークン保有者がETHを請求できるインターフェース。
各トークン保有者は特定のスナップショットで収益を1回だけ請求できるため、公平で透明な分配メカニズムが保証されます。

## burnメカニズム

トークン保有者がトークンを`burn`して収益プールから比例分配されたシェを取得できるメカニズムを実装しています。
このメカニズムは、トークンの価値が常に収益プールのシェア以上であることを保証し、トークンの供給量が減少することでトークンの価値を高めることができます。

## redeemableOnBurnインターフェース

トークンを`burn`した時に、トークン所有者が取得できるETHの量を計算するインターフェース。
`view`関数として定義され、以下の例では`redeemableETH`トークン保有者が`amount`分のトークンを`burn`することで取得可能なETHの量を計算しています。

```math
redeemableETH = amount / totalSupply * totalRedeemableETH
```

`totalSupply`はトークンの双供給量を示し、`totalRedeemableETH`はプール内のETHの総量を示します。

## burnインターフェース

トークン保有者がトークンを`burn`して収益を受け取るインターフェース。
トークン保有者は、保有するトークンをすべて`burn`することができ、これによりトークンの総供給量が減りデフレ経済モデルが確立されます。
また、一度`burn`されたトークンはその後収益分配を受け取る対象から除外されます。

# 互換性

この規格は**ERC20**と互換性があります。

# 参考実装

```js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./IERC7641.sol";

contract ERC7641 is ERC20Snapshot, IERC7641 {
    /**
     * @dev last snapshotted block
     */
    uint256 private _lastSnapshotBlock;

    /**
     * @dev percentage claimable
     */
    uint256 immutable public percentClaimable;

    /**
     * @dev mapping from snapshot id to address to the amount of ETH claimable at the snapshot.
     */
    mapping (uint256 => uint256) private _claimableAtSnapshot;

    /**
     * @dev mapping from snapshot id to a boolean indicating whether the address has claimed the revenue.
     */
    mapping (uint256 => mapping (address => bool)) private _claimedAtSnapshot;

    /**
     * @dev claim pool
     */
    uint256 private _claimPool;

    /**
     * @dev burn pool
     */
    uint256 private _burnPool;

    /**
     * @dev burned from new revenue
     */
    uint256 private _burned;

    /**
     * @dev Constructor for the ERC7641 contract, premint the total supply to the contract creator.
     * @param name The name of the token
     * @param symbol The symbol of the token
     * @param supply The total supply of the token
     */
    constructor(string memory name, string memory symbol, uint256 supply, uint256 _percentClaimable) ERC20(name, symbol) {
        require(_percentClaimable <= 100, "ERC7641: percentage claimable should be less than 100");
        percentClaimable = _percentClaimable;
        _lastSnapshotBlock = block.number;
        _mint(msg.sender, supply);
    }

    /**
     * @dev A function to calculate the amount of ETH claimable by a token holder at certain snapshot.
     * @param account The address of the token holder
     * @param snapshotId The snapshot id
     * @return The amount of revenue token claimable
     */
    function claimableRevenue(address account, uint256 snapshotId) public view returns (uint256) {
        uint256 balance = balanceOfAt(account, snapshotId);
        uint256 totalSupply = totalSupplyAt(snapshotId);
        uint256 ethClaimable = _claimableAtSnapshot[snapshotId];
        return _claimedAtSnapshot[snapshotId][account] ? 0 : balance * ethClaimable / totalSupply;
    }

    /**
     * @dev A function for token holder to claim revenue token based on the token balance at certain snapshot.
     * @param snapshotId The snapshot id
     */
    function claim(uint256 snapshotId) public {
        uint256 claimableETH = claimableRevenue(msg.sender, snapshotId);
        require(claimableETH > 0, "ERC7641: no claimable ETH");

        _claimedAtSnapshot[snapshotId][msg.sender] = true;
        _claimPool -= claimableETH;
        (bool success, ) = msg.sender.call{value: claimableETH}("");
        require(success, "ERC7641: claim failed");
    }
    
    /**
     * @dev A function to claim by a list of snapshot ids.
     * @param snapshotIds The list of snapshot ids
     */
    function claimBatch(uint256[] memory snapshotIds) public {
        for (uint256 i = 0; i < snapshotIds.length; i++) {
            claim(snapshotIds[i]);
        }
    }
    
    /**
     * @dev A snapshot function that also records the deposited ETH amount at the time of the snapshot.
     * @return The snapshot id
     * @notice example requirement: only 1000 blocks after the last snapshot
     */
    function snapshot() public returns (uint256) {
        require(block.number - _lastSnapshotBlock > 1000, "ERC7641: snapshot interval is too short");
        uint256 snapshotId = _snapshot();
        _lastSnapshotBlock = block.number;
        
        uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool;

        uint256 claimableETH = newRevenue * percentClaimable / 100;
        _claimableAtSnapshot[snapshotId] = claimableETH;
        _claimPool += claimableETH;
        _burnPool += newRevenue - claimableETH - _burned;
        _burned = 0;

        return snapshotId;
    }

    /**
     * @dev A function to calculate the amount of ETH redeemable by a token holder upon burn
     * @param amount The amount of token to burn
     * @return The amount of revenue ETH redeemable
     */
    function redeemableOnBurn(uint256 amount) public view returns (uint256) {
        uint256 totalSupply = totalSupply();
        uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool;
        uint256 burnableFromNewRevenue = amount * (newRevenue * (100 - percentClaimable) - _burned * 100) / 100 / totalSupply;
        uint256 burnableFromPool = amount * _burnPool / totalSupply;
        return burnableFromNewRevenue + burnableFromPool;
    }

    /**
     * @dev A function to burn tokens and redeem the corresponding amount of revenue token
     * @param amount The amount of token to burn
     */
    function burn(uint256 amount) public {
        uint256 totalSupply = totalSupply();
        uint256 newRevenue = address(this).balance + _burned - _claimPool - _burnPool;
        uint256 burnableFromNewRevenue = amount * (newRevenue * (100 - percentClaimable) - _burned * 100)  / 100 / totalSupply;
        uint256 burnableFromPool = amount * _burnPool / totalSupply;
        _burnPool -= burnableFromPool;
        _burned += burnableFromNewRevenue;
        _burn(msg.sender, amount);
        (bool success, ) = msg.sender.call{value: burnableFromNewRevenue + burnableFromPool}("");
        require(success, "ERC7641: burn failed");
    }

    receive() external payable {}
}
```

# セキュリティ

## デフレ経済モデル

トークンが`burn`されることによりトークン価値の向上メカニズムに関する懸念事項があります。
`burn`によりトークン供給量の継続的な現象は、流動性の低下を引き起こします。
流動性の低下は価格ボラティリティを増加させ、価格操作のリスクを高めます。
対策としては、徹底的なストレステストと市場シミュレーションの実行が挙げられます。

## スパム収益トークン

AltRevToken拡張により、異なる収益トークンを設定できると価値のないトークンが複数追加される可能性があります。
これにより、`burn`および請求プロセスに関するガスコストが向上し、ユーザーがのエコシステムへの参加を阻害する可能性があります。
対策としては、新しい収益トークンの承認と追加に関するガバナンスモデルを導入し、透明でコミュニティ主導の意思決定プロセスを実施して有用なトークンのみが追加されることが挙げられます。

:::note info
具体的な手順としては以下になります。
1. コミュニティ投票
    - 新しいトークンの追加にはコミュニティの投票を行います。
2. セキュリティ監査
    2. トークンの追加前にセキュリティ監査を実施します。
3. ガス料金の影響の考慮
    3. 新しいトークンがガス料金に与える影響を慎重に評価します。
:::


# 引用

Conway (@0x1cc), Cathie So (@socathie), Xiaohang Yu (@xhyumiracle), Suning Yao (@fewwwww), Kartin <kartin@hyperoracle.io>, "ERC-7641: Intrinsic RevShare Token [DRAFT]," Ethereum Improvement Proposals, no. 7641, February 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7641.

# 最後に

今回は「**ERC20**トークンを拡張して、収益分配メカニズムを統合して持続可能なトークンモデルの仕組みを提案している**ERC7641**」についてまとめてきました！
いかがだったでしょうか？

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