[ERC4974] コントラクト内でレートを管理する仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、コントラクト内で**レート**を管理する提案をしている規格である**ERC4974**についてまとめていきます！

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

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

:::note warn
**4974**は現在（2023年9月13日）では「**Review**」段階です。
:::

# 概要

この規格は、Ethereumブロックチェーン上で**レート**を管理するための基準を設定したものです。
**レート**とは、何かのアイテムやプロジェクトに対する数値的な評価やランキングのことを指します。
例えば、製品の評価、アート作品の評価、あるいはプロジェクトの成功度などが評価の対象になります。

この規格によって、**レート**のデータをブロックチェーン上に保存し、信頼性の高い方法で管理できるようになります。
また、他のアプリケーションも同じデータにアクセスできるため、様々なアプリケーションでこの評価データを利用することができます。

これは、トークンに対して新しい使い道を生み出すことができます。
例えば、特定の**レート**を持つトークンを発行し、それに基づいて特典を提供するアプリケーションが考えられます。
また、プロジェクトの評価をトークンとして取引する市場も形成されるかもしれません。

# 動機

これまで、ブロックチェーンアプリケーションはデジタルアセット（例：仮想通貨やトークン）の購入と販売に主に焦点を当ててきました。
しかし、このアセット中心のモデルには欠点があり、特にコミュニティベースのブロックチェーンプロジェクトにとって問題となってきました。
2021年には、EVM（Ethereum Virtual Machine）ベースのゲームやDAO（分散型自治組織）などで、支払いをすることで特典を得るというダイナミクスが問題視されました。

この提案は、**レート**をスマートコントラクトやウォレットに割り当てることを可能にすることで、この問題に対処しようとしています。
つまり、ブロックチェーンアプリケーションに新しい組み合わせ可能な要素を提供するものです。
これにより、以下のような新しい使い道が考えられます。

### DAO内での投票権
この規格を使用して割り当てられた**レート**をもとに、分散型自治組織（DAO）のメンバーの投票権を決定できます。
例えば、DAOはコミュニティへの貢献が高いメンバーに高い**レート**を与え、これを利用して各メンバーの意思決定プロセスへの影響力を決めることができます。

### 分散型ゲームエコシステム内での経験ポイント
**レート**は、分散型ゲームエコシステム内のプレイヤーの進捗を追跡し、特定の目標を達成した時に報酬を提供するために使用できます。
例えば、ゲームはプレイヤーに**レート**を与え、これを使って新しいコンテンツや能力をアンロックできるようにすることができます。

### ビジネスの顧客向けのロイヤルティポイント
**レート**は、特定のビジネスやサービスに対する顧客の忠誠心を追跡し、忠誠度に応じた報酬を提供するために使用できます。
例えば、ビジネスは評価を顧客に割り当て、これを特別なオファーや割引と交換できるようにすることができます。

### 分散型保険会社のアセット評価
**レート**は、分散型保険会社内のアセットのリスクプロファイルを評価し、保険料やカバレッジを決定するために使用できます。
例えば、分散型保険会社はさまざまなアセットのリスクを評価し、低リスクのアセットに対しては低い保険料と高いカバレッジを提供できるように**レート**を利用できます。

この規格は、**EIP20**や**EIP721**といったトークン規格を参考にしています。

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

https://chaldene.net/erc20

:::note info
**ERC721**は以下を参考にしてください。
:::

https://chaldene.net/erc721

# 仕様

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

```javascript
// SPDX-License-Identifier: CC0

pragma solidity ^0.8.0;

/// @title EIP-4974 Ratings
/// @dev See https://eips.ethereum.org/EIPS/EIP-4974
///  Note: the EIP-165 identifier for this interface is #######.
///  Must initialize contracts with an `operator` address that is not `address(0)`.
interface IERC4974 /* is ERC165 */ {

    /// @dev Emits when operator changes.
    ///  MUST emit when `operator` changes by any mechanism.
    ///  MUST ONLY emit by `setOperator`.
    event NewOperator(address indexed _operator);

    /// @dev Emits when operator issues a rating. 
    ///  MUST emit when rating is assigned by any mechanism.
    ///  MUST ONLY emit by `rate`.
    event Rating(address _rated, int8 _rating);

    /// @dev Emits when operator removes a rating. 
    ///  MUST emit when rating is removed by any mechanism.
    ///  MUST ONLY emit by `remove`.
    event Removal(address _removed);

    /// @notice Appoint operator authority.
    /// @dev MUST throw unless `msg.sender` is `operator`.
    ///  MUST throw if `operator` address is either already current `operator`
    ///  or is the zero address.
    ///  MUST emit an `Appointment` event.
    /// @param _operator New operator of the smart contract.
    function setOperator(address _operator) external;

    /// @notice Rate an address.
    ///  MUST emit a Rating event with each successful call.
    /// @param _rated Address to be rated.
    /// @param _rating Total EXP tokens to reallocate.
    function rate(address _rated, int8 _rating) external;

    /// @notice Remove a rating from an address.
    ///  MUST emit a Remove event with each successful call.
    /// @param _removed Address to be removed.
    function removeRating(address _removed) external;

    /// @notice Return a rated address' rating.
    /// @dev MUST register each time `Rating` emits.
    ///  SHOULD throw for queries about the zero address.
    /// @param _rated An address for whom to query rating.
    /// @return int8 The rating assigned.
    function ratingOf(address _rated) external view returns (int8);
}

interface IERC165 {
    /// @notice Query if a contract implements an interface.
    /// @dev Interface identification is specified in EIP-165. This function
    ///  uses less than 30,000 gas.
    /// @param interfaceID The interface identifier, as specified in EIP-165.
    /// @return bool `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise.
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
```

# 補足

## レートの割り当て
**レート**はコントラクト運営者の独自の裁量によって行われます。
運営者は、スポーツチームのコーチであるか、マルチシグ（複数の署名が必要な）DAOウォレットなどさまざまです。
具体的なガバナンス方法は指定しませんが、ガバナンスが存在することだけを規定します。
これにより、特定の意思決定形式に最適化するのではなく、より幅広い潜在的な用途が可能になります。

## int8の選択
符号つきの整数（`int8`）を選択しました。
これは、レビュアーがウォレットやコントラクトに対して中立的でネガティブな評価を行えるようにするためです。
特に悪意のある行為が発生する可能性がある分散型アプリケーションにとって重要です。
`8`ビットの整数を使用することで、評価を比較可能な範囲内に保つことが目的です。
長期的には、評価の簡単な集計を促進することができ、ユーザーがさまざまなスケールを使用することが少なくなります。

:::note info
整数（`int`）は、数値データを表現するためのデータ型の一種です。
符号つき整数（`int`）は、正負の数を表現できるもので、例えば`0`、`1`、`-1`、`2`、`-2`などが含まれます。
8ビットの整数（`int8`）は、8つのビット（`0`または`1`のバイナリ桁）で数値を表現することができるものです。

**例 1**
ウォレットやのコントラクトの**レート**を`int8`型として扱う場合、評価は`-128`から`127`までの範囲で表現できます。
これは、`8`ビットが表現できる範囲です。

**例 2**
レビュアーが特定の分散型アプリケーションに対して評価を行うと考えましょう。
**レート**は以下のような数値で表現されるかもしれません。
- `0`
    - 中立的な評価
- `1`~`127`
    - 肯定的な評価（`1`が最低の肯定的な評価、`127`が最高の肯定的な評価）
- `-1`~`-128`
    - ネガティブな評価（`-1`が最低のネガティブな評価、`-128`が最高のネガティブな評価）

**例 3**
複数のユーザーが同じウォレットに対して評価を行うとします。
それぞれのユーザーは、中立的な評価、肯定的な評価、またはネガティブな評価を`0`~`127`または`-1`~`-128`の範囲内で割り当てることができます。

**例 4**
アプリケーションが複数のユーザーの**レート**を収集し、集計する場合、`int8`型を使用することで、異なるユーザーの評価を容易に合算できます。
例えば、100人のユーザーがウォレットに対して評価を行い、それぞれの評価が`0`~`127`までの範囲内である場合、これらの評価を単純に合計することで、ウォレットの総合**レート**を計算できます。

`8`ビットの整数は、**レート**を比較可能な形式で保持し、集計するための効果的な方法を提供します。
ユーザーがさまざまなスケールを使用する必要がないため、**レート**の統一性を保つのに役立ちます。
:::

## レートの変更
**レート**はコントラクト運営者によって更新できるようにするべきです。
たとえば、Bobがコミュニティに大きな貢献をしたが、後にAliceから盗みを働いたと発覚した場合、コミュニティはBobの地位と影響力を下げるべきかどうかを決定できます。
これはコミュニティ内で倫理的な基準を確保することはできませんが、その可能性を開くものです。
関連して、**レート**は、評価者が効果的に評価できる自信がない場合に**レート**を取り消すことを許可するべきです。

:::note info
**例 1: レートの更新**
ある分散型コミュニティにおいて、Bobというメンバーがコミュニティに大きな貢献をし、その**レート**が高いことで知られています。
しかし、ある日、BobがAliceから盗みを働いたという報告が寄せられました。コミュニティはこの問題を対処する必要があります。

- Bobの**レート**が高いことは、Bobのコミュニティ内での地位や影響力を示しています。
- コミュニティは、**レート**を更新することを決定します。
- これにより、Bobの**レート**が低下し、彼の地位や影響力が減少します。
- この変更は、コミュニティのメンバーや運営者によってコントラクト内で実行され、Bobの行動に応じて**レート**が変更され、コミュニティは倫理的な基準を確保し、問題のあるメンバーに対処できるようになります。

**例 2: レートの取り消し**
評価者としての自信を持てないユーザーがいるかもしれません。
このユーザーは、誤って**レート**を付けたり、誤った**レート**を付けた場合に、**レート**を取り消す機能が必要です。

- ユーザーAは、特定のコントラクトに**レート**を付けることを試みますが、**レート**の正確さに自信がありません。
- ユーザーAは誤って高い**レート**をつけたことに気付き、その**レート**を取り消すことを選択します。
- システムはユーザーAに**レート**を取り消すオプションを提供し、ユーザーAが誤った**レート**を修正できるようにします。

これにより、ユーザーは誤った**レート**を調整し、**レート**の精度を向上させることができます。
この機能は、**レート**プロセスの信頼性を高め、誤解や不正確な**レート**を修正できるようにします。
:::

## インターフェースの検出
この規格に準拠するスマートコントラクトがサポートするインターフェースを公開するために、標準インターフェース検出（**EIP165**）を選択しました。
   
## メタデータの選択
この規格では、スマートコントラクトに関する情報を提供するために「名前（`name`）」と「説明（`description`）」が格納されたメタデータが必要です。
これは、他のブロックチェーン規格と一貫性を持たせるためで、主要なブロックチェーンプリミティブにおいて一般的な要素です。
また、複数の**レート**システムが存在する場合、詳細な説明を提供する「`description`」機能も提供されています。
実装者がこれらの情報を提供しない場合、空の文字列を使用できますが、その場合でも正当な応答です。
また、同じ名前と説明を使用する他のスマートコントラクトが存在する可能性にも留意する必要があります。

## デメリット
この規格を使用する際の潜在的なデメリットは、**レート**が主観的であるため、コントラクトやウォレットの真の価値や品質を常に正確に反映しない可能性があることです。
しかし、この規格は**レート**を更新および削除する機能を提供し、柔軟性を持たせています。
ユーザーは他のコントラクトやコミュニティがどのように評価しているかを知る必要があり、評価によって誇りを感じる場合もあれば、不当なネガティブ評価を受ける可能性もあります。
不当な評価は悪意のある行動を引き起こす可能性があるため、**レート**システムを作成する実装者には慎重に対処する責任があります。

# 後方互換性

**EIP20**および**EIP721**仕様の`name`の定義を採用しています。

# 参考実装

```javascript
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;
import "./IERC4974.sol";

/**
 * See {IERC4974}
 * Implements the ERC4974 Metadata extension.
 */
contract LoyaltyPoints is IERC4974 {

    // The address of the operator that can assign ratings
    address private _operator;

    // Mapping of customer addresses to their ratings
    mapping (bytes32 => int8) private _ratings;

    // Initializes the contract by setting the operator to msg.sender
    constructor () {
        _operator = msg.sender;
    }

    // Set the operator address
    // Only the current operator or the contract owner can call this function
    function setOperator(address newOperator) public override {
        require(_operator == msg.sender || msg.sender == address(this), "Only the current operator or the contract owner can set the operator.");
        _operator = newOperator;
        emit NewOperator(_operator);
    }

    // Rate a customer
    // Only the operator can call this function
    function rate(address customer, int8 rating) public override {
        require(_operator == msg.sender, "Only the operator can assign ratings.");
        bytes32 hash = keccak256(abi.encodePacked(customer));
        _ratings[hash] = rating;
        emit Rating(customer, rating);
    }

    // Remove a rating from a customer
    // Only the operator can call this function
    function removeRating(address customer) external override {
        require(_operator == msg.sender, "Only the operator can remove ratings.");
        bytes32 hash = keccak256(abi.encodePacked(customer));
        delete _ratings[hash];
        emit Removal(customer);
    }

    // Get the rating for a customer
    function getOperator() public view returns (address) {
        return _operator;
    }

    // Check if a customer has been rated
    function hasBeenRated(address customer) public view returns (bool) {
        // Hash the customer address
        bytes32 hash = keccak256(abi.encodePacked(customer));

        // Check if the hash exists in the mapping
        return _ratings[hash] != 0;
    }

    function ratingOf(address _rated) public view override returns (int8) {
        bytes32 hash = keccak256(abi.encodePacked(_rated));
        // Check if the customer has been rated
        require(hasBeenRated(_rated), "This customer has not been rated yet.");
        // Return the customer's rating
        return _ratings[hash];
    }

    // Award ETH to a customer based on their rating
    function awardEth(address payable customer) public payable {
        // Calculate the amount of ETH to award based on the customer's rating
        int8 rating = ratingOf(customer);
        require(rating > 0, "Sorry, this customer has a rating less than 0 and cannot be awarded.");
        uint256 award = uint256(int256(rating));
        // Transfer the ETH to the customer
        require(address(this).balance >= award, "Contract has insufficient balance to award ETH.");
        customer.transfer(award);
    }

    receive () external payable {}

}
```

### `_operator`

```javascript
address private _operator;
```

**概要**
評価を割り当てることができるオペレーター（運営者）のアドレス。

**詳細**
このスマートコントラクト内で評価を割り当てる権限を持つアカウントです。
オペレーターは他のユーザーに対して**レート**を与えることができます。

### `_ratings`

```javascript
mapping (bytes32 => int8) private _ratings;
```

**概要**
アドレスに対応する**レート**を格納する配列。

**詳細**
ユーザーごとに**レート**を記録するために使用されます。
ユーザーのアドレス（`bytes32`）とそれに対応する**レート**（`int8`、符号つき整数）が関連付けられます。
ユーザーの評価は、オペレーターによって割り当てられ、必要に応じて更新または削除することができます。

---

### `setOperator`

```javascript
function setOperator(address newOperator) public override {
    require(_operator == msg.sender || msg.sender == address(this), "Only the current operator or the contract owner can set the operator.");
    _operator = newOperator;
    emit NewOperator(_operator);
}
```

**概要**
オペレーターアドレスを設定する関数。

**詳細**
オペレーターは**レート**を割り当てるための権限を持つアカウントであり、この関数を使用して新しいオペレーターアドレスを設定できます。
オペレーターまたはコントラクトの所有者でない場合、この関数を呼び出すことはできません。
新しいオペレーターアドレスが設定されると、`NewOperator`イベントが発行されます。

**引数**
- `newOperator`
    - 新しいオペレーターのアドレス。

### `rate`

```javascript
function rate(address customer, int8 rating) public override {
    require(_operator == msg.sender, "Only the operator can assign ratings.");
    bytes32 hash = keccak256(abi.encodePacked(customer));
    _ratings[hash] = rating;
    emit Rating(customer, rating);
}
```

**概要**
顧客に**レート**を割り当てる関数。

**詳細**
**レート**は特定のアドレスに関連付けられ、対応するハッシュに保存されます。
**レート**を割り当てる権限を持つオペレーターのみがこの関数を呼び出すことができます。
**レート**が割り当てられると、`Rating`イベントが発行されます。

**引数**
- `customer`
    - **レート**を受けるアドレス。
- `rating`
    - 特定アドレスに割り当てる**レート**（`-128`~`127`までの範囲内の整数）

### `removeRating`

```javascript
function removeRating(address customer) external override {
    require(_operator == msg.sender, "Only the operator can remove ratings.");
    bytes32 hash = keccak256(abi.encodePacked(customer));
    delete _ratings[hash];
    emit Removal(customer);
}
```

**概要**
指定された特定のアドレスの**レート**を削除する関数。

**詳細**
**レート**が削除されると、対応するハッシュが削除され、**レート**がリセットされます。
**レート**が削除されると、`Removal`イベントが発行されます。
**レート**を割り当てる権限を持つオペレーターのみがこの関数を呼び出すことができます。


**引数**
- `customer`
    - **レート**を削除する特定のアドレス。

### `getOperator`

```javascript
function getOperator() public view returns (address) {
    return _operator;
}
```

**概要**
現在のオペレーターのアドレスを取得する関数。

**詳細**
他のユーザーが現在のオペレーターを確認できるようにするための関数です。

**戻り値**
- `address`
    - 現在のオペレーターのアドレス。

### `hasBeenRated`

```javascript
function hasBeenRated(address customer) public view returns (bool) {
    // Hash the customer address
    bytes32 hash = keccak256(abi.encodePacked(customer));

    // Check if the hash exists in the mapping
    return _ratings[hash] != 0;
}
```

**概要**
指定された特定のアドレスが評価を受けたか確認する関数。

**詳細**
指定された顧客のアドレスをハッシュ化し、それがマッピング内に存在するかどうかを確認します。
存在する場合、該当アドレスは評価を受けたことがあります。

**引数**
- `customer`
    - **レート**を確認する特定のアドレス。

**戻り値**
- `bool`
    - **レート**が存在する場合は`true`、存在しない場合は`false`。

### `ratingOf`

```javascript
function ratingOf(address _rated) public view override returns (int8) {
    bytes32 hash = keccak256(abi.encodePacked(_rated));
    // Check if the customer has been rated
    require(hasBeenRated(_rated), "This customer has not been rated yet.");
    // Return the customer's rating
    return _ratings[hash];
}
```

**概要**
指定された特定のアドレスの**レート**を取得する関数。

**詳細**
指定された特定のアドレスをハッシュ化し、**レート**が存在することを確認します。
**レート**が存在する場合、顧客の**レート**を返します。
**レート**が存在しない場合、エラーメッセージが表示されます。

**引数**
- `_rated`
    - **レート**を取得する特定のアドレス。

**戻り値**
- `int8`
    - 特定のアドレスの**レート**（`-128`~`127`までの範囲内の整数）。

### `awardEth`

```javascript
function awardEth(address payable customer) public payable {
    // Calculate the amount of ETH to award based on the customer's rating
    int8 rating = ratingOf(customer);
    require(rating > 0, "Sorry, this customer has a rating less than 0 and cannot be awarded.");
    uint256 award = uint256(int256(rating));
    // Transfer the ETH to the customer
    require(address(this).balance >= award, "Contract has insufficient balance to award ETH.");
    customer.transfer(award);
}
```

**概要**
指定された特定アドレスに対して**ETH**を付与します。

**詳細**
特定アドレスの**レート**に基づいて付与額が計算され、評価が`0`より大きい場合にのみ付与が行われ、**ETH**を特定アドレスに転送します。
また、コントラクト内の**ETH**残高が付与額以上あるかを確認します。

**引数**
- `customer`
    - **ETH**を付与する特定のアドレス。

# セキュリティ考慮事項


# 引用

Daniel Tedesco (@dtedesco1), "ERC-4974: Ratings [DRAFT]," Ethereum Improvement Proposals, no. 4974, April 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4974.

# 最後に

今回は「コントラクト内で**レート**を管理する提案をしている規格である**ERC4974**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58