[ERC3525] ERC721とERC20の特徴を併せ持つ半代替性トークンの仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、ERC721とERC20の機能を併せ持つ半代替性トークンと呼ばれている**ERC3525**についてまとめていきます！

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

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

# 概要

この提案は、セミファンジブルトークン（semi-fungible tokens）の標準についてまとめたものです。
この文書で説明されているスマートコントラクトインターフェースのセットは、**ERC721**互換のトークン標準を定義しています。

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

https://chaldene.net/erc721

この標準は、「`ID`」、「`SLOT`」、「`VALUE`」という**トリプルスカラーモデル**を導入し、トークンのセミファンジブルな構造を表現します。
また、セミファンジブルな性質を反映したトークンの新しい`Transfer`（送付）モデルと`Approve`（承認）モデルも導入しています。

トークンは**ERC721**と同等のIDプロパティ（トークンID)を持ち、トークン自体を一意のエンティティとして識別するために使用されます。
これにより、トークンをアドレス間で転送したり、**ERC721**互換の方法で特定のアドレスを`Approve`（承認）したりできます。

トークンには、数量的な性質を表す`value`プロパティも含まれています。
この`value`プロパティの意味は、**ERC20**トークンの`balance`プロパティとかなり似ています。

:::note info
**ERC20**の`balance`プロパティとは、特定のアドレスが**ERC20**トークンをどれくらい所有しているかを保持しているものです。
:::

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

https://chaldene.net/erc20

各トークンには`slot`属性があり、同じスロットを持つ2つのトークンの値はファンジブル（同等）として扱われるため、トークンの`value`プロパティにファンジビリティ（代替性）を付加します。

このEIPでは、セミファンジビリティに対応した新しいトークン`Transfer`（送付）モデルを導入しています。
これには、同じ`slot`の2つのトークン間での値の転送や、トークンからアドレスへの値の転送が含まれます。

![トリプルスカラーモデル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/2e3b9d6f-ff8c-f60e-5766-555597269be3.png)

:::note info
`ID`、`VALUE`、`SLOT`については上の図のような関係性になっています。
- `ID`
    - トークンを一位に識別するための値。
    - **ERC721**と互換性を持たせるために重要な機能。
- `VALUE`
    - トークンの量を表す。
    - 量は数という意味合いだけでなく、ゲームにおいての「**攻撃力**」や「**HP**」として使用することもできる。
- `SLOT`
    - トークンが所属するカテゴリーやグループを示す値。
    - 同じスロットのトークンは価値が同じと見なされ交換が可能。
:::


# 動機

トークン化は、暗号通貨におけるデジタル資産の利用と管理において重要なトレンドの一つです。
従来、トークン化には、FT（代替性トークン: **ERC20**）とNFT（非代替性トークン: **ERC721**）の2つのアプローチがありました。
FTは資産としての価値が同等なため交換することができます。
一方、NFTは識別子（トークンID）に基づいてデジタル資産を区別することができます。

しかし、両者には重要な欠点があります。
例えば、**ERC20**はユーザーが個々のデータ構造やカスタマイズ可能なプロパティの組み合わせごとに、別々の**ERC20**コントラクトを作成する必要があります。
これにより、膨大な数の**ERC20**コントラクトが作成されることになります。
一方、**ERC721**トークンは数量的な特徴を持たないため、計算、流動性、および管理性に大きな制約があります。
例えば、**ERC721**を使用して債券、保険ポリシー、またはベスティングなどの金融商品を作成する場合、トークンを送付したりトークンの一部を分割して送付することは難しいです。

数量的な**ERC20**の特徴と、識別的な**ERC721**の属性を持つセミファンジブルトークンを作成することで、上記の問題を解決しようとしています。
**ERC3525**という新しいトークンスタンダードでは、トークンの値を部分的に`Transfer`（送付）するための新しい`Transfer`（送付）モデルが導入され、例えば債券の一部の価値を別のアドレスにTransfer`（送付）することが可能になるります。

![Transfer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/bb544832-a637-985e-25cf-c4276c46af10.png)

# 仕様

すべての**ERC3525**に準拠したコントラクトは、**ERC3525**、**ERC721**、**ERC165**の各インターフェースを実装する必要があります。

## IERC3525

```javascript
pragma solidity ^0.8.0;

interface IERC3525 /* is IERC165, IERC721 */ {
    event TransferValue(uint256 indexed _fromTokenId, uint256 indexed _toTokenId, uint256 _value);

    event ApprovalValue(uint256 indexed _tokenId, address indexed _operator, uint256 _value);

    event SlotChanged(uint256 indexed _tokenId, uint256 indexed _oldSlot, uint256 indexed _newSlot);

    function valueDecimals() external view returns (uint8);

    function balanceOf(uint256 _tokenId) external view returns (uint256);

    function slotOf(uint256 _tokenId) external view returns (uint256);

    function approve(
        uint256 _tokenId,
        address _operator,
        uint256 _value
    ) external payable;

    function allowance(uint256 _tokenId, address _operator) external view returns (uint256);

    function transferFrom(
        uint256 _fromTokenId,
        uint256 _toTokenId,
        uint256 _value
    ) external payable;


    function transferFrom(
        uint256 _fromTokenId,
        address _to,
        uint256 _value
    ) external payable returns (uint256);
}
```

### TransferValue

トークンの`Value`が別いのトークンに`Transfer`（送付）されたときに発行されるイベント。
トークンが他のトークンの`Value`を`Transfer`（送付）する場合や、`Value`が`0`のトークン間で`Transfer`（送付）される場合にも発行されます。

- `_fromTokenId`
    - `VALUE`の`Transfer`（送付）元のトークンID。
- `_toTokenId`
    - `VALUE`の`Transfer`（送付）先のトークンのID。
- `_value`
    - `Transfer`（送付）される`VALUE`の量。

### ApprovalValue

トークンの`Transfer`（送付）が承認された`VALUE`の設定、または変更されときに発行されるイベント。
トークンの所有者、承認されたオペレータが、特定のトークンの`VALUE`を管理するために呼び出すことができる関数によって発行されます。

- `_tokenId`
    - 承認されるトークンID。
- `_operator`
    - 承認されるオペレータのアドレス。
- `_value`
    - `_operator`が管理できる最大`VALUE`。

### SlotChanged

トークンの`SLOT`が設定または変更されたときに発行されるイベント。
トークンの`SLOT`属性が変更されたときにこのイベントが発行されます。

- `_tokenId`
    - `SLOT`が設定または変更されるトークンID。
- `_oldSlot`
    - トークンの変更前の`SLOT`。
- `_newSlot`
    - トークンの更新された`SLOT`。

### valueDecimals

トークンの`VALUE`を表現する際の小数点以下の桁数を返す関数。
例えば、小数点以下6桁の場合、`1,000,000`で`VALUE`を割ることで`VALUE`の値を取得できます。
**ERC20**トークンとの互換性を考慮して、この関数は`valueDecimals()`という名前になっています。

### balanceOf

特定のトークンの`VALUE`を取得する関数。

- `_tokenId`
    - `VALUE`を取得する対象のトークンID。

### slotOf

特定のトークンの`SLOT`（属性）を取得する関数。
- `_tokenId`
    - `SLOT`を取得する対象のトークンID。

### approve

特定のトークンに対して、指定されたアドレス（`_operator`）が最大でいくらの`VALUE`を管理できるかを許可する関数。

- `_tokenId`
    - 承認するトークンID。
- `_operator`
    - 承認するオペレータのアドレス。
- `_value`
    - `_operator`が管理できる最大`VALUE`。
トークンの所有者、承認されたオペレータが、特定のトークンの`VALUE`を管理するために呼び出され、`ApprovalValue`イベントが発行されます。

### allowance

特定のトークンに対して、指定されたアドレス（`_operator`）が現在許可されている`VALUE`を取得する関数。

- `_tokenId`
    - `VALUE`の許可を確認する対象のトークンID。
- `_operator`
    - 許可を確認するアドレス。

### transferFrom

特定のトークンから別のトークンに`VALUE`を`Transfer`（送付）する関数。
`Transfer`（送付）元と`Transfer`（送付）先のトークンは同じ`SLOT`を持っている必要があります。

- `_fromTokenId`
    - `VALUE`を`Transfer`（送付）する元のトークンID。
- `_toTokenId`
    - `VALUE`を`Transfer`（送付）する先のトークンID。
- `_value`
    - `Transfer`（送付）する`VALUE`の量。
呼び出し元は、トークンの所有者、承認されたオペレータのアドレスである必要があります。
また、トークンの`VALUE`の範囲内で`Transfer`（送付）を行う必要があります。
`Transfer`（送付）が成功した場合、`TransferValue`イベントが発行されます。

### transferFrom

特定のトークンから指定されたアドレス（`_to`）に`VALUE`を`Transfer`（送付）する関数。
呼び出し元は、トークンの`VALUE`を受け取るアドレス（`_to`）が**ERC3525**トークンを受け取ることができることを確認する必要があります。

- `_fromTokenId`
    - `VALUE`の`Transfer`（送付）元のトークンID。
- `_to`
    - `VALUE`を受け取るアドレス。
- `_value`
    - `Transfer`（送付）する`VALUE`の量。
新しい**ERC3525**トークンを作成するか、指定されたアドレス（`_to`）が既に同じ`SLOT`のトークンを持っている場合は、そのトークンに`VALUE`を`Transfer`（送付）します。
`Transfer`（送付）が成功した場合、`Transfer`および`TransferValue`イベントが発行されます。

## IERC3525SlotEnumerable

`SLOT`の列挙拡張はオプション機能です。
これにより、コントラクトは`SLOT`の全リストを公開し値を取得できるようになります。

```javascript
pragma solidity ^0.8.0;

interface IERC3525SlotEnumerable is IERC3525 /* , IERC721Enumerable */ {

    function slotCount() external view returns (uint256);

    function slotByIndex(uint256 _index) external view returns (uint256);

    function tokenSupplyInSlot(uint256 _slot) external view returns (uint256);

    function tokenInSlotByIndex(uint256 _slot, uint256 _index) external view returns (uint256);
}
```

### slotCount

コントラクトに保存されている`SLOT`の総数を取得する関数。
戻り値は`SLOT`の総数。

### slotByIndex

コントラクトに保存されている全ての`SLOT`の中から、指定されたインデックスに対応する`SLOT`を取得する関数。
`_index`パラメーターに、取得したい`SLOT`のインデックスを指定します。
戻り値は、指定されたインデックスに対応する`SLOT`を表す整数です。

### tokenSupplyInSlot

特定の`SLOT`に属するトークンの総数を取得する関数。
- `_slot`
    - 取得したい`SLOT`。
戻り値は、指定された`SLOT`に属するトークンの総数。

### tokenInSlotByIndex

特定の`SLOT`に属するトークンの中から、指定されたインデックスに対応するトークンIDを取得する関数。

- `_slot`
    - 取得した`SLOT`。
- `_index`
    - 取得したいトークンのインデックス。
戻り値は、指定されたインデックスに対応するトークンID。

## IERC3525SlotApprovable

`SLOT`レベルでの承認はオプション機能です。
これにより、`SLOT`の承認をサポートしたいコントラクトは、オペレーターが同じ`SLOT`で自分のトークンを管理できるようになります。

```javascript
pragma solidity ^0.8.0;

interface IERC3525SlotApprovable is IERC3525 {
    event ApprovalForSlot(address indexed _owner, uint256 indexed _slot, address indexed _operator, bool _approved);

    function setApprovalForSlot(
        address _owner,
        uint256 _slot,
        address _operator,
        bool _approved
    ) external payable;

    function isApprovedForSlot(
        address _owner,
        uint256 _slot,
        address _operator
    ) external view returns (bool);
}
```

### ApprovalForSlot

特定のオペレータが、特定のアドレスが所有する全ての`SLOT`内のトークンを管理できるように承認されるか、または承認が取り消されるときに発行されるイベント。

- `_owner`
    - トークン所有者のアドレス。
- `_slot`
    - 承認される`SLOT`。
- `_operator`
    - 承認または取り消しを行われるオペレータのアドレス。
- `_approved`
    - `_operator`が承認されている場合は`true`、取り消されている場合は`false`を表す。


### setApprovalForSlot

特定の所有者の特定の`SLOT`内の全てのトークンを管理する権限を、他アドレスに承認または取り消すための関数。
呼び出し元は、`_owner`か`setApprovalForAll`を通じてオペレータとして認可されている必要があります。
この関数を実行すると、`ApprovalForSlot`イベントが発行されます。

- `_owner`
    - **ERC3525**トークンの所有者のアドレス。
- `_slot`
    - 承認または取り消しを行う`SLOT`。
- `_operator`
    - 承認または取り消すオペレータのアドレス。
- `_approved`
    - `_operator`を承認する場合は`true`、取り消す場合は`false`を指定。

### isApprovedForSlot

特定のアドレスが特定の`SLOT`内の全てのトークンを管理できる権限があるか確認する関数。
戻り値は、`_operator`のアドレスが指定された`SLOT`の全てのトークンを管理する権限がある場合は`true`、それ以外の場合は`false`を返す。

- `_owner`
    - **ERC3525**トークンの所有者アドレス。
- `_slot`
    - 確認する`SLOT`。
- `_operator`
    - 確認するオペレータのアドレス。

## ERC3525トークンレシーバー
スマートコントラクトが他のアドレスから値を受け取ったときに通知を受けたい場合、`IERC3525Receiver`インターフェースのすべての関数を実装する必要があります。

```javascript
 pragma solidity ^0.8.0;

interface IERC3525Receiver {
    function onERC3525Received(address _operator, uint256 _fromTokenId, uint256 _toTokenId, uint256 _value, bytes calldata _data) external returns (bytes4);

}
```

### onERC3525Received

**ERC3525**トークンの値が受け取られたときに呼び出される関数。
**ERC3525**トークンを受け取るスマートコントラクトは、この関数を実装しているかどうかをチェックする必要があります。
もし受け取るコントラクトがこの関数を実装している場合、**ERC3525**トークンコントラクトは、値の送付（`transferFrom(uint256,uint256,uint256,bytes)`）の後にこの関数を呼び出す必要があります。
この関数は、トランスファーが受け入れられた場合は`0x009ce20b`（`bytes4(keccak256('onERC3525Received(address,uint256,uint256,uint256,bytes)'))`）を返さなければなりません。
`Transfer`（送付）が拒否された場合は、`0x009ce20b`以外の値を返すか、リバートしなければなりません。


- `_operator`
    - `Transfer`（送付）を実行したアドレス。
- `_fromTokenId`
    - 値を`Transfer`（送付）するトークンID。
- `_toTokenId`
    - 値の`Transfer`（送付）先のトークンID。
- `_value`
    - `Transfer`（送付）される値。
- `_data`
    - 指定されたフォーマットのない追加のデータ

## トークンの操作

このEIPでは、**ERC721**互換のトークン`Transfer`（送付）関数に加えて、2つの新しい`Transfer`（送付）モデルが導入されています。
具体的には、IDからIDへの値の`Transfer`（送付）と、IDからアドレスへの値の`Transfer`（送付）です。

```javascript
function transferFrom(uint256 _fromTokenId, uint256 _toTokenId, uint256 _value) external payable;
	
function transferFrom(uint256 _fromTokenId, address _to, uint256 _value) external payable returns (uint256 toTokenId_);
```

1. **`transferFrom(uint256 _fromTokenId, uint256 _toTokenId, uint256 _value)`**
同じ`SLOT`内の別のトークン（`_fromTokenId`で指定されたトークン）から別のトークン（`_toTokenId`で指定されたトークン）への値の`Transfer`（送付）を行う関数。
`_value`の値が元のトークンの値から差し引かれ、宛先トークンの値に加算されます。

2. **`transferFrom(uint256 _fromTokenId, address _to, uint256 _value)`**
1つのトークン（`_fromTokenId`で指定されたトークン）からアドレス（`_to`で指定されたアドレス）への値の`Transfer`（送付）を許可します。
実際には、値はアドレスが所有するトークンに`Transfer`（送付）され、宛先トークンのIDが返される必要があります。

### 承認ルール

このEIPでは4つの種類の承認関数が提供されており、以下のそれぞれ異なるレベルの承認を示しています。

**フルレベルの承認**
`setApprovalForAll`関数。
**ERC721**と互換性があり、所有者が保有するすべてのトークン、およびその値を含む、すべてのトークンを管理できるようにするフルレベルの承認を許可します。

**スロットレベルの承認**
`setApprovalForSlot`関数。
スロットレベルの承認を示すものであり、指定された`SLOT`を持つすべてのトークンとそれに紐づく値の承認を許可します。

**トークンIDレベルの承認**
所有者が所有する特定のトークンID、およびその値を管理できるようにする。

**値レベルの承認**
指定されたトークンの`VALUE`の指定量の承認を許可する。

どの承認関数も、所有者であるか、より高いレベルの権限を持っているアドレスから実行される必要があります。

### transferFromルール

1. **`transferFrom(uint256 _fromTokenId, uint256 _toTokenId, uint256 _value)`**
1つのトークンから別のトークンへの値の`Transfer`（送付）を示すもので、以下のルールに従う必要がある。
  - `msg.sender`が`_fromTokenId`の所有者、承認されたオペレータ、または`_value`の量のトークンの承認を受けたオペレータである場合にのみ成功し、それ以外の場合は`revert`する。
  - `_fromTokenId`または`_toTokenId`が`0`のトークンIDであるか、存在しない場合は`revert`する。
  - `_fromTokenId`と`_toTokenId`の`SLOT`が一致しない場合は`revert`します。
  - `_value`が`_fromTokenId`の値またはオペレータへの許可を超える場合は`revert`する。
  - `_toTokenId`の所有者がスマートコントラクトの場合は、`onERC3525Received`関数をチェックし、関数が存在する場合は値の`Transfer`（送付）後にこの関数を呼び出し、結果が`0x009ce20b`と等しくない場合は`revert`する。
  - `TransferValue`イベントを発行する。

2. **`transferFrom(uint256 _fromTokenId, address _to, uint256 _value)`**
1つのトークンIDからアドレスへの値の`Transfer`（送付）を示すもので、以下のルールに従う必要がある。
  - `_to`アドレスが所有する**ERC3525**トークンを見つけるか、新しい**ERC3525**トークンを同じ`SLOT`の`_fromTokenId`として作成して、`Transfer`（送付）された値を受け取る必要がある。
  - `msg.sender`が`_fromTokenId`の所有者、承認されたオペレータ、または少なくとも`_value`の量のトークンの承認を受けたオペレータである場合にのみ成功し、それ以外の場合は`revert`する。
  - `_fromTokenId`が`0`のトークンIDであるか、存在しない場合は`revert`する。
  - `_to`がゼロアドレスの場合は`revert`する。
  - `_value`が`_fromTokenId`の値、またはオペレータへの許可を超える場合は`revert`する。
  - `_to`アドレスがスマートコントラクトの場合は、`onERC3525Received`関数をチェックし、関数が存在する場合は値の`Transfer`（送付）後にこの関数を呼び出し、結果が`0x009ce20b`と等しくない場合は`revert`する。
  - `Transfer`および`TransferValue`イベントを発行する。

## メタデータ

### メタデータ拡張子
**ERC3525**メタデータ拡張機能は、**ERC721**メタデータ拡張機能と互換性があります。
このオプションのインターフェイスは、**ERC165**標準インターフェイス検出で識別できます。

```javascript
pragma solidity ^0.8.0;

interface IERC3525Metadata is
    IERC3525 /* , IERC721Metadata */
{
    function contractURI() external view returns (string memory);
    function slotURI(uint256 _slot) external view returns (string memory);
}
```

**contractURI**

現在の**ERC3525**コントラクトのURIを取得する関数。
戻り値は、JSON形式のURIを返します。

**slotURI**

指定された`SLOT`のURIを取得する関数。
戻り値は、JSON形式のURIを返します。

### ERC3525メタデータ URI JSON スキーマ

- `contractURI`の戻り値。
```json
{
  "title": "Contract Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Contract Name"
    },
    "description": {
      "type": "string",
      "description": "Describes the contract"
    },
    "image": {
      "type": "string",
      "description": "Optional. Either a base64 encoded imgae data or a URI pointing to a resource with mime type image/* representing what this contract represents."
    },
    "external_link": {
      "type": "string",
      "description": "Optional. A URI pointing to an external resource."
    },
    "valueDecimals": {
      "type": "integer",
      "description": "The number of decimal places that the balance should display - e.g. 18, means to divide the token value by 1000000000000000000 to get its user representation."
    }
  }
}
```


- `slotURI`の戻り値。

```json
{
  "title": "Slot Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the asset category to which this slot represents"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset category to which this slot represents"
    },
    "image": {
      "type": "string",
      "description": "Optional. Either a base64 encoded imgae data or a URI pointing to a resource with mime type image/* representing the asset category to which this slot represents."
    },
    "properties": {
      "type": "array",
      "description": "Each item of `properties` SHOULD be organized in object format, including name, description, value, order (optional), display_type (optional), etc."
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The name of this property."
          },
          "description": {
            "type": "string",
            "description": "Describes this property."
          }
          "value": {
            "description": "The value of this property, which may be a string or a number."
          },
          "is_intrinsic": {
            "type": "boolean",
            "description": "According to the definition of `slot`, one of the best practice to generate the value of a slot is utilizing the `keccak256` algorithm to calculate the hash value of multi properties. In this scenario, the `properties` field should contain all the properties that are used to calculate the value of `slot`, and if a property is used in the calculation, is_intrinsic must be TRUE."
          },
          "order": {
            "type": "integer",
            "description": "Optional, related to the value of is_intrinsic. If is_intrinsic is TRUE, it must be the order of this property appeared in the calculation method of the slot."
          },
          "display_type": {
            "type": "string",
            "description": "Optional. Specifies in what form this property should be displayed."
          }
        }
      }
    }
  }
}
```

- `tokenURI`の戻り値。

```json
{
  "title": "Token Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the asset to which this token represents"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset to which this token represents"
    },
    "image": {
      "type": "string",
      "description": "Either a base64 encoded imgae data or a URI pointing to a resource with mime type image/* representing the asset to which this token represents."
    },
    "balance": {
      "type": "integer",
      "description": "THe value held by this token."
    },
    "slot": {
      "type": "integer",
      "description": "The id of the slot that this token belongs to."
    },
    "properties": {
      "type": "object",
      "description": "Arbitrary properties. Values may be strings, numbers, objects or arrays. Optional, you can use the same schema as the properties section of ERC-3525 Metadata JSON Schema for slotURI(uint) if you need a better description attribute."
    }
  }
}
```

# 補足

## メタデータの生成
このトークン標準は、コレクタブルやゲームアイテムよりも金融商品に適した準交換性のあるアセットを表現するように設計されています。
デジタルアセットの透明性と安全性を確保するために、すべての実装はメタデータをオフチェーンのサーバーURLではなく、コントラクトコードから直接生成することを強く推奨しています。

## トークンからアドレスへの値のTransfer
トークンの「`VALUE`（値）」はトークン自体のプロパティであり、アドレスとは関連していないため、`VALUE`をアドレスに直接`Transfer`（送付）することは、そのアドレスが所有するトークンに`VALUE`を`Transfer`（送付）することを意味します。

実装の観点から、トークンからアドレスへの`VALUE`の`Transfer`（送付）は次のように行われる可能性があります。

1. 受け取りアドレスに新しいトークンを作成する。
2. ソーストークンから新しいトークンに`VALUE`を`Transfer`（送付）する。

これにより、この関数はIDからIDへの`Transfer`（送付）メソッドと完全に独立しているわけではなく、上記のプロセスをラップしたものと見なすことができます。

特別な場合として、宛先アドレスがソーストークンと同じ`SLOT`値を持つ1つ以上のトークンを所有している場合、このメソッドは以下のように代替実装がされます。

1. アドレスが所有するトークンのうち、ソーストークンと同じ`SLOT`を見つける。
2. 見つかった`SLOT`にトークンに`Transfer`（送付）する。

上記の両方の実装は、この標準に準拠して扱われるべきです。

IDからアドレスへの`Transfer`（送付）関数を維持する目的は、ほとんどのウォレットアプリとの互換性のためです。
ほとんどのトークン標準では、トークンの`Transfer`（送付）先はアドレスであるため、この構文のラッピングはトークンから任意のアドレスに`VALUR`を`Transfer`（送付）する機能をウォレットアプリが簡単に実装できるようにします。

## SafeTransferの代わりの通知/承認メカニズム
**ERC721**と一部の互換性のあるトークン標準では、「`SafeTransfer`」モデルが導入されていますが、このメカニズムは送信者に異なる`Tranfer`モード（`Transfer` / `SafeTransferFrom`）の選択を委ねるため、以下のような潜在的な問題を引き起こす可能性があります。

- 送信者は通常、2つの`Tranfer`メソッド（`Transfer` / `SafeTransferFrom`）のどちらを選択すべきかわらない。
- 送信者が`safeTransferFrom`メソッドを呼び出すと、受信者のコントラクトがコールバック関数を実装していない場合、そのコントラクトが問題なくトークンを受け取っても`Transfer`処理が失敗する可能性があります。

このEIPは、柔軟性と簡単さのためにシンプルな「**Check、Notify and Response**」モデルを定義しています。

追加の`safeTransferFrom`メソッドは必要ありません。
すべての呼び出し元は1種類の`Transfer`を呼び出すだけで済みます。
すべての**ERC3525**コントラクトは、受け取りコントラクトに`onERC3525Received`が存在するかどうかをチェックし、存在する場合はその関数を呼び出さなければなりません。
任意のスマートコントラクトは、値を受け取った後に通知される目的で`onERC3525Received`関数を実装できます。
この関数は、`Transfer`（送付）が受け入れられた場合は`0x009ce20b`（`bytes4(keccak256('onERC3525Received(address,uint256,uint256,uint256,bytes)'))`）を返し、`Transfer`（送付）が拒否された場合は他の値を返します。

この通知/承認メカニズムには特別なケースがあります。
**ERC3525**はアドレスから自身への値`VALUE`の`Transfer`（送付）を許可しているため、`onERC3525Received`を実装したスマートコントラクトが`VALUE`を自身に`Transfer`（送付）する場合、`onERC3525Received`も呼び出されます。
これにより、コントラクトは自身に`Transfer`（送付）する機能と他のアドレスからの値の受け取りとの間で異なる受け入れのルールを実装できます。

## 異なる承認モデル間の関係
**ERC721**と**ERC3525**トークン標準の互換性を持たせるため、トークンの値を操作する柔軟性を考慮して、次のような承認レベルの関係を定義しました。

IDの承認により、承認されたオペレーターがこのIDから部分的な`VALUE`を`Transfer`（送付）できるようになります。
これにより、IDの`VALUE`の承認が簡素化されます。
ただし、全てのトークンの承認では、所有権を他のアドレスに与えないように注意する必要があります。
`setApprovalForAll`により、任意のトークンから部分的な`VALUE`を`Transfer`（送付）する権限、および任意のトークンから第三者に対して部分的な`VALUE`の`Transfer`（送付）を承認する権限が得られます。
これにより、アドレスが所有するすべてのトークンの`VALUE`の`Transfer`（送付）と承認が簡素化されます。

# 後方互換性:
このEIPは、**ERC721**と後方互換性があります。

# 参考実装:

公式の実装は以下にあります。

https://github.com/ethereum/EIPs/tree/master/assets/eip-3525/

上記の実装をもとに以下のディレクトリ内にコードをまとめました。

https://github.com/cardene777/base-code/tree/develop/contract/contracts/ERC3525

# テスト

以下の公式のテストコードをほとんどそのまま持ってきたものを格納しています。

https://github.com/cardene777/base-code/blob/develop/contract/test/ERC3525/ERC3525.test.ts

```sh
$ cd contract
$ npm install
$ npx hardhat test test/ERC6147/ERC6147.test.ts
```
上記コマンドでテストの実行ができます。

# セキュリティに関する考慮事項:
`VALUE`レベルの承認と`SLOT`レベルの承認（オプション）は**ERC721**の承認モデルとは独立しています。
したがって、このEIPの実装はこの原則に従う必要があります。

このEIPは**ERC721**互換であるため、標準的な**ERC721**トークンを保持し操作することができるすべてのウォレットとスマートコントラクトは、非互換の標準実装による**ERC3525**トークンの資産損失のリスクはありません。

# 引用

Will Wang (@will42w), Mike Meng <myan@solv.finance>, Yi Cai (@YeeTsai) <yee.tsai@gmail.com>, Ryan Chow <ryanchow@solv.finance>, Zhongxin Wu (@Nerverwind), AlvisDu (@AlvisDu), "ERC-3525: Semi-Fungible Token," Ethereum Improvement Proposals, no. 3525, December 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3525.

# 考察

**ERC3525**は金融系に使用されることを考えられていると書かれていましたが、もっと色々な場面で使用できそうな規格です。
例えばコミュニティを作成し、SBTと呼ばれる譲渡不可能トークンにしたのち、コミュニティ内にサブコミュニティを作成して`SLOT`で所属を表しながら、各メンバーのコミュニティの貢献ポイントを`VALUE`として持たせるなどできそうですよね。
前半でも書かれていたように、**ERC3525**はNFTの価値を分割することができます。
例えば、ゲーム内のキャラクターをNFTにして、所有しているNFTキャラクターを貸し出せるとします。
貸し出すキャラクターのパワーが`1000`ある場合、`400`と`600`のパワーを持つ2体に分割してそれぞれを貸し出すとかもできそうな気がします。
分割できんなかったNFTが、実質分割したかのように表現できるようになったので、その強みを活かして何か面白そうなことができそうですね。

# 最後に

今回は「ERC721とERC20の機能を併せ持つ半代替性トークンと呼ばれている**ERC3525**」についてまとめてきました！
いかがだったでしょうか？

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

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

# 採用強化中！

CryptoGamesでは一緒に働く仲間を大募集中です。

https://www.wantedly.com/companies/cryptogames

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう！