[ERC5216] 所有しているERC1155トークンの一部を他アドレスへ承認できる仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC1155**規格を拡張して、より細かくトークンの`approve`を実行できる提案である**ERC5216**についてまとめていきます！

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

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

:::note warn
**ERC5216**は現在（2023年9月3日）では「**Last Call**」段階です。
:::

# 概要

**EIP1155**は、同じトークンIDを持つ複数のトークンを効率的に管理する標準インターフェースを提供する企画です。

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

https://chaldene.net/erc1155

# 動機
この規格では、トークンの識別子（`id`）と数量に基づいてより細かい承認プロセスを実行する提案がされています。
**ERC1155**は、**ERC721**の仕組みで提案されている各`id`に数量というデータが紐づくトークン形式です。

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

https://chaldene.net/erc721

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

https://chaldene.net/erc1155

ブロックチェーンでは、プログラムのエラーや悪意のある操作によって資金が永久に失われる可能性があるため、安全にトランザクションが実行される必要があります。
**ERC1155**は、`setApprovalForAll`関数を使用しており、特定のアドレスが保有する`id`のトークンを一括して`approve`します。

:::note info
`setApprovalForAll`関数は、所有者が特定のアドレス（オペレーター）に対して、自分のトークンの操作権限を一括で与える関数です。

例えば、NFT（Non-Fungible Token）の所有者である「**Alice**」が、NFTマーケットプレイスの「**Opensea**」に自分のトークンを出品したいとします。
この場合、**Alice**は `setApprovalForAll`関数を使用して、**Opensea**に対して自分のNFTを操作する権限を与えることができます。

```solidity
// NFTの所有者
address public owner = msg.sender;

// NFTマーケットプレイスのアドレス
address public marketplace = 0x...; // ダミーアドレス

// NFTのトークンID
uint256 public tokenId = 123;

// Marketplaceに対してトークンの操作権限を一括で与える
function grantPermissionToMarketplace() public {
    // ownerがmarketplaceに対して操作権限を一括で与える
    approve(marketplace, true);
}

// 操作権限を取り消す
function revokePermissionFromMarketplace() public {
    // ownerがmarketplaceに対して操作権限を取り消す
    approve(marketplace, false);
}

// アドレスに対して操作権限を一括で与える関数
function approve(address operator, bool approval) private {
    // ERC-1155のsetApprovalForAll関数を呼び出す
    IERC1155(tokenContractAddress).setApprovalForAll(operator, approval);
}
```

上記の例では、**Alice**は自分のNFTを**Opensea**に出品するために、`grantPermissionToMarketplace`関数を呼び出し、**Opensea**に対して操作権限を一括で与えます。
その後、NFTが売却される時に、**Opensea**は**Alice**の操作権限を使用してトークンを移動できます。
不要になった場合、**Alice**は `revokePermissionFromMarketplace`関数を呼び出すことで、**Opensea**に対する操作権限を取り消すことができます。
:::

しかし、この方法では最低限の信頼性を確保するには不足しています。
この提案では、**ERC20**と**ERC721**のアイデアを組み合わせて、所有者がマーケットプレイスなどの第三者に対して、特定の`id`に対して指定された数のトークンを`approve`する仕組みを提案しています。

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

https://chaldene.net/erc20

# 仕様

このEIPを使用するコントラクトは、**IERC1155ApprovalByAmount**インタフェースを実装する必要があります。

## インターフェース実装

```javascript
/**
 * @title ERC-1155 Approval By Amount Extension
 * Note: the ERC-165 identifier for this interface is 0x1be07d74
 */
interface IERC1155ApprovalByAmount is IERC1155 {

    /**
     * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
     * `id` and with an amount: `amount`.
     */
    event ApprovalByAmount(address indexed account, address indexed operator, uint256 id, uint256 amount);

    /**
     * @notice Grants permission to `operator` to transfer the caller's tokens, according to `id`, and an amount: `amount`.
     * Emits an {ApprovalByAmount} event.
     *
     * Requirements:
     * - `operator` cannot be the caller.
     */
    function approve(address operator, uint256 id, uint256 amount) external;

    /**
     * @notice Returns the amount allocated to `operator` approved to transfer `account`'s tokens, according to `id`.
     */
    function allowance(address account, address operator, uint256 id) external view returns (uint256);
}
```


- `approve(address operator, uint256 id, uint256 amount)`関数は、`public`または`external`である必要があります。
- `allowance(address account, address operator, uint256 id)`関数は、`public`または`external`かつ`view`である必要があります。
- `safeTrasferFrom`関数（**EIP1155**で定義されたもの）は、以下の条件を満たす必要があります。
  - ユーザーが`msg.sender`に十分なトークン量を承認している場合、リバート（`revert`）してはいけない。
  - `msg.sender`が`setApprovalForAll`で承認されていない場合、送りたいトークン量のうち承認済みではない数量を引く必要がある。

:::note info
`msg.sender`はトークンの持ち主ではなく、トークンの持ち主からトークンのやり取りをする権限を与えられているアドレスです。
`setApprovalForAll`で承認されている場合は、持ち主の全てのトークンをやり取りできますが、そうでない場合は持ち主の一部のトークンのみやり取りできます（もしくは1つも承認されていないか）。
そのため、送りたいトークン量のうち、承認されているトークン量だけ送ることができます。
:::

- `safeBatchTransferFrom`関数には、次の追加条件が必要です。
  - 送りたいトークンの全ての`id`に対して、送りたいトークン量が承認されているか確認する条件を追加する必要があります。

:::note info
`safeBatchTransferFrom`関数は、複数のトークンを安全に一括で転送する関数です。
具体的を用いて説明します。

例えば、ユーザー「**Bob**」が所有する複数の異なるNFT（Non-Fungible Token）を、別のユーザー「**Alice**」に一括で転送する場面を考えてみます。

```solidity
// ERC-1155のトークンコントラクトアドレス
address public tokenContractAddress = 0x...; // ダミーアドレス

// Bobのアドレス
address public bob = msg.sender;

// Aliceのアドレス
address public alice = 0x...; // ダミーアドレス

// 転送するトークンの情報
uint256[] public tokenIds = [1, 2, 3]; // 仮のトークンID
uint256[] public amounts = [1, 1, 1]; // 各トークンの数量

// BobがAliceにトークンを一括で転送する
function transferTokensToAlice() public {
    // ERC-1155のsafeBatchTransferFrom関数を呼び出す
    IERC1155(tokenContractAddress).safeBatchTransferFrom(bob, alice, tokenIds, amounts, "");
}
```

上記の例では、**Bob**は自分が所有する複数のNFTを一括で**Alice**に転送するために、`transferTokensToAlice`関数を呼び出しています。
`safeBatchTransferFrom`関数は、**Bob**のアドレスから**Alice**のアドレスへ、指定したトークンIDと数量のトークンを一括で転送します。

`safeBatchTransferFrom`関数は、複数の異なるトークンを一括で転送する場合に便利な機能です。
関数の呼び出しによって、指定されたトークンIDと数量のトークンが安全に転送されます。
:::


- 一定数のトークンが承認された場合、`ApprovalByAmount`イベントが発行します。
- `supportsInterface`メソッドは、`0x1be07d74`を引数として呼び出された際に、`true`を返します。

# 補足

**"EIP-1155 Approval By Amount Extension"**という名前は、このEIPの簡潔な説明を提供するために選ばれました。
ユーザーは特定のトークンのidと数量を指定して、オペレーターに対してトークンの承認を行うことができる機能を拡張するためのEIPです。

**EIP20**に似た方法でトークンの承認と取り消しを行うことにより、ユーザーはより直接的にトークンの管理を行うことができるようになります。

- `approve`関数を使用することで、ユーザーは特定のオペレーターに対して、各トークンのidごとに一定の数量のトークンの使用を許可することができます。
- `allowance`関数を使用することで、ユーザーは特定のオペレーターが各トークンのidごとにどれだけの承認を持っているかを確認できます。

この名前の選択は、**EIP1155**で提案されているトークンの承認の仕組みが、既存の**EIP20**と似た考え方に基づいており、トークン管理のプロセスを理解しやすくするためです。
これにより、ユーザーは自分のトークンの操作をより管理しやすくなります。


# 後方互換性
この標準は、**EIP1155**と互換性があります。

# 参考実装

以下が参考実装になります。

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.15;

import "IERC1155.sol";
import "ERC1155.sol";

/**
 * @title ERC-1155 Approval By Amount Extension
 * Note: the ERC-165 identifier for this interface is 0x1be07d74
 */
interface IERC1155ApprovalByAmount is IERC1155 {

    /**
     * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
     * `id` and with an amount: `amount`.
     */
    event ApprovalByAmount(address indexed account, address indexed operator, uint256 id, uint256 amount);

    /**
     * @notice Grants permission to `operator` to transfer the caller's tokens, according to `id`, and an amount: `amount`.
     * Emits an {ApprovalByAmount} event.
     *
     * Requirements:
     * - `operator` cannot be the caller.
     */
    function approve(address operator, uint256 id, uint256 amount) external;

    /**
     * @notice Returns the amount allocated to `operator` approved to transfer `account`'s tokens, according to `id`.
     */
    function allowance(address account, address operator, uint256 id) external view returns (uint256);
    
}

/**
 * @dev Extension of {ERC1155} that allows you to approve your tokens by amount and id.
 */
abstract contract ERC1155ApprovalByAmount is ERC1155, IERC1155ApprovalByAmount {

    // Mapping from account to operator approvals by id and amount.
    mapping(address => mapping(address => mapping(uint256 => uint256))) internal _allowances;

    /**
     * @dev See {IERC1155ApprovalByAmount}
     */
    function approve(address operator, uint256 id, uint256 amount) public virtual {
        _approve(msg.sender, operator, id, amount);
    }

    /**
     * @dev See {IERC1155ApprovalByAmount}
     */
    function allowance(address account, address operator, uint256 id) public view virtual returns (uint256) {
        return _allowances[account][operator][id];
    }

    /**
     * @dev safeTransferFrom implementation for using ApprovalByAmount extension
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) public override(IERC1155, ERC1155) {
        require(
            from == msg.sender || isApprovedForAll(from, msg.sender) || allowance(from, msg.sender, id) >= amount,
            "ERC1155: caller is not owner nor approved nor approved for amount"
        );
        unchecked {
            _allowances[from][msg.sender][id] -= amount;
        }
        _safeTransferFrom(from, to, id, amount, data);
    }

    /**
     * @dev safeBatchTransferFrom implementation for using ApprovalByAmount extension
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override(IERC1155, ERC1155) {
        require(
            from == msg.sender || isApprovedForAll(from, msg.sender) || _checkApprovalForBatch(from, msg.sender, ids, amounts),
            "ERC1155: transfer caller is not owner nor approved nor approved for some amount"
        );
        _safeBatchTransferFrom(from, to, ids, amounts, data);
    }

    /**
     * @dev Checks if all ids and amounts are permissioned for `to`. 
     *
     * Requirements:
     * - `ids` and `amounts` length should be equal.
     */
    function _checkApprovalForBatch(
        address from, 
        address to, 
        uint256[] memory ids,
        uint256[] memory amounts
    ) internal virtual returns (bool) {
        uint256 idsLength = ids.length;
        uint256 amountsLength = amounts.length;

        require(idsLength == amountsLength, "ERC1155ApprovalByAmount: ids and amounts length mismatch");
        for (uint256 i = 0; i < idsLength;) {
            require(allowance(from, to, ids[i]) >= amounts[i], "ERC1155ApprovalByAmount: operator is not approved for that id or amount");
            unchecked { 
                _allowances[from][to][ids[i]] -= amounts[i];
                ++i; 
            }
        }
        return true;
    }

    /**
     * @dev Approve `operator` to operate on all of `owner` tokens by id and amount.
     * Emits a {ApprovalByAmount} event.
     */
    function _approve(
        address owner,
        address operator,
        uint256 id,
        uint256 amount
    ) internal virtual {
        require(owner != operator, "ERC1155ApprovalByAmount: setting approval status for self");
        _allowances[owner][operator][id] = amount;
        emit ApprovalByAmount(owner, operator, id, amount);
    }
}

contract ExampleToken is ERC1155ApprovalByAmount {
    constructor() ERC1155("") {}

    function mint(address account, uint256 id, uint256 amount, bytes memory data) public {
        _mint(account, id, amount, data);
    }

    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) public {
        _mintBatch(to, ids, amounts, data);
    }
}

```

### approve

```javascript
function approve(address operator, uint256 id, uint256 amount) public virtual {
    _approve(msg.sender, operator, id, amount);
}
```

**概要**
所有者が特定のオペレーターに対して、指定されたトークンIDと数量に基づいてトークンの使用を許可する関数。

**詳細**
所有者は、この関数を使用してオペレーターに対してトークンの使用権を付与することができます。
関数は、指定されたオペレーターに対して、特定のトークンIDごとに一定の数量のトークンの使用を許可します。
この関数は内部で`_approve`関数を呼び出して実際の承認処理を行います。

**引数**
- `operator`
    - トークンの操作権限を与える対象のアドレス（オペレーター）。
- `id`
    - 操作権限を与えるトークンID。
- `amount`
    - 操作権限を与えるトークンの数量。

---

### allowance

```javascript
function allowance(address account, address operator, uint256 id) public view virtual returns (uint256) {
    return _allowances[account][operator][id];
}
```

**概要**
指定されたアカウントとオペレーターの組み合わせに対して、特定のトークンIDの許可されたトークン数量を取得する関数。

**詳細**
指定されたアカウントとオペレーターの組み合わせに対して、特定のトークンIDに対して許可されたトークン数量を返します。
これにより、ユーザーは特定のオペレーターが特定のトークンに対してどれだけの操作権限を持っているかを確認できます。

**引数**
- `account`
    - 許可情報を取得するアカウントのアドレス。
- `operator`
    - アカウントに対するオペレーターのアドレス。
- `id`
    - 許可情報を取得するトークンID。

**戻り値**
特定のアカウントとオペレーターの組み合わせに対して、指定されたトークンIDの許可されたトークン数量を示す整数値。

---

### safeTransferFrom

```javascript
function safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) public override(IERC1155, ERC1155) {
    require(
        from == msg.sender || isApprovedForAll(from, msg.sender) || allowance(from, msg.sender, id) >= amount,
        "ERC1155: caller is not owner nor approved nor approved for amount"
    );
    unchecked {
        _allowances[from][msg.sender][id] -= amount;
    }
    _safeTransferFrom(from, to, id, amount, data);
}
```

**概要**
`ApprovalByAmount`拡張を使用した安全なトークンの転送を行う関数。

**詳細**
指定された条件を満たす場合にトークンを転送します。
条件としては、以下が含まれます。
- `from`が`msg.sender`であるか、`msg.sender`によって`from`から`isApprovedForAll`で承認されている。
- `from`が`msg.sender`に対して`id`に指定されたトークンの`amount`に基づいて十分な承認を持っている。

転送処理の前に、オペレーターに対する許可された数量が減算されます。
その後、`_safeTransferFrom`関数を呼び出して安全なトークン転送を行います。

**引数**
- `from`
    - トークンの所有者のアドレス。
- `to`
    - トークンを受け取るアドレス。
- `id`
    - 転送するトークンID。
- `amount`
    - 転送するトークンの数量。
- `data`
    - 任意の追加データ。

---

### safeBatchTransferFrom

```javascript
function safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) public virtual override(IERC1155, ERC1155) {
    require(
        from == msg.sender || isApprovedForAll(from, msg.sender) || _checkApprovalForBatch(from, msg.sender, ids, amounts),
        "ERC1155: transfer caller is not owner nor approved nor approved for some amount"
    );
    _safeBatchTransferFrom(from, to, ids, amounts, data);
}
```

**概要**
`ApprovalByAmount`拡張を使用した安全にトークンを一括転送を行う関数。

**詳細**
指定された条件を満たす場合に複数のトークンを一括で転送します。
条件としては、以下が含まれます。
- `from`が`msg.sender`であるか、`msg.sender`によって`from`から`isApprovedForAll`で承認されている。
- `from`が`msg.sender`に対して`id`に指定された複数トークンごとに、各トークンの`amount`に基づいて十分な承認を持っている。

転送処理の前に、オペレーターに対する許可された数量が一括で減算されます。
その後、`_safeBatchTransferFrom`関数を呼び出して安全な一括トークン転送を行います。

**引数**
- `from`
    - トークンの所有者のアドレス。
- `to`
    - トークンを受け取るアドレス。
- `ids`
    - 転送するトークンのidの配列。
- `amounts`
    - 転送するトークンの数量の配列。
- `data`
    - 任意の追加データ。


---

### _checkApprovalForBatch

```javascript
function _checkApprovalForBatch(
    address from, 
    address to, 
    uint256[] memory ids,
    uint256[] memory amounts
) internal virtual returns (bool) {
    uint256 idsLength = ids.length;
    uint256 amountsLength = amounts.length;

    require(idsLength == amountsLength, "ERC1155ApprovalByAmount: ids and amounts length mismatch");
    for (uint256 i = 0; i < idsLength;) {
        require(allowance(from, to, ids[i]) >= amounts[i], "ERC1155ApprovalByAmount: operator is not approved for that id or amount");
        unchecked { 
            _allowances[from][to][ids[i]] -= amounts[i];
            ++i; 
        }
    }
    return true;
}
```

**概要**
トークン一括転送の時に指定されたトークンIDと数量が許可されているかを確認する関数。

**詳細**
指定されたアカウント`from`からアカウント`to`への一括トークン転送の時に、各トークンIDと数量が適切に許可されているかを確認します。
指定された`ids`配列と`amounts`配列の要素数が一致していることを確認し、各トークンに対する許可情報を確認します。
許可情報が適切であることが確認されると、許可された数量を減算します。

**引数**
- `from`
    - トークンの所有者のアドレス。
- `to`
    - トークンを受け取るアドレス。
- `ids`
    - 転送するトークンのidの配列。
- `amounts`
    - 転送するトークンの数量の配列。

**戻り値**
トークンのidと数量が適切に許可されている場合に`true`を返します。

---

### _approve

```javascript
function _approve(
    address owner,
    address operator,
    uint256 id,
    uint256 amount
) internal virtual {
    require(owner != operator, "ERC1155ApprovalByAmount: setting approval status for self");
    _allowances[owner][operator][id] = amount;
    emit ApprovalByAmount(owner, operator, id, amount);
}
```

**概要**
所有者がオペレーターに対して特定のトークンIDと数量に基づいて承認を行う関数。

**詳細**
指定された所有者のアドレス`owner`がオペレーターのアドレス`operator`に対して、特定のトークンIDに対して指定された数量を承認します。
ただし、所有者とオペレーターが同じアドレスである場合は、自己に対する承認を行うことはできません。
承認情報は`_allowances`配列内に設定され、`ApprovalByAmount`イベントが発行されます。

**引数**
- `owner`
    - トークンの所有者のアドレス。
- `operator`
    - トークンを操作するオペレーターのアドレス。
- `id`
    - 承認するトークンID。
- `amount`
    - 承認するトークンの数量。


---

# セキュリティ考慮事項
このEIPのユーザーは、オペレーターに対して許可するトークンの数量を十分に検討し、使用されない許可を取り消すことを考慮する必要があります。

:::note info
ユーザーがトークンの許可を適切に管理し、不要な権限を取り消すことが重要であることを強調しています。
:::

# 引用

Iván Mañús (@ivanmmurciaua), Juan Carlos Cantó (@EscuelaCryptoES), "ERC-5216: EIP-1155 Approval By Amount Extension [DRAFT]," Ethereum Improvement Proposals, no. 5216, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5216.

# 最後に

今回は「**ERC1155**規格を拡張して、より細かくトークンの`approve`を実行できる提案である**ERC5216**」についてまとめてきました！
いかがだったでしょうか？

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

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