はじめに

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

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

今回は、ERC1155規格のセキュリティにおいて脆弱性があったり、不便な部分を解決する提案であるERC6909についてまとめていきます!

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

ERC6909は現在(2023年9月3日)では「Draft」段階です。

概要

この提案は、ERC-1155 Multi-Token Standardのシンプルな代替案として、マルチトークンコントラクトを定義しています。

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

動機

ERC1155という規格には、いくつかの追加的な機能が含まれています。 例えば、受け取りアドレスがコントラクトの場合、特定の値を返すコールバックを実装する必要があります。

トークンの受け取りアドレスがコントラクトの場合、ERC1155規格ではトークンの転送やバッチ転送が行われる際に、トークンの受け取りアドレスであるコントラクトに特定の値を返すコールバックが実装される必要があります。 このコールバック機能は、トークンの受け取りアドレスがトークンを受け取ったり転送されたりする時に、受け取りアドレスのコントラクトに予め決まった処理を行わせるためのものです。

具体的には、トークンの転送が行われると、受け取りアドレスのコントラクト内に定義された特定の関数(コールバック関数)が実行されます。 この関数は、受け取りアドレスがトークンを受け取る際に実行されるため、受け取りアドレスのコントラクトはその処理に必要な特定の値を返す必要があります。 これにより、トークンの転送や操作が透明に行われ、トークンを受け取る側のコントラクトにカスタムのロジックを組み込むことができます。

ただし、このコールバック機能は受け取りアドレスのコントラクトに外部コードの呼び出しを伴うため、ガスの消費や実行時間などの面で注意が必要です。 また、一部の状況では、トークンを転送する側と受け取る側のコントラクト間でコールバックのバージョン管理や互換性の確保が必要になることもあります。

具体的なコードでみていきます。

javascript
// ERC-1155トークンコントラクト
contract MyERC1155Token {
    // トークンIDごとの残高を管理するマップ
    mapping(address => mapping(uint256 => uint256)) private balances;

    // トークン転送時のコールバック関数
    function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes memory _data)
        public returns (bytes4)
    {
        // 特定の値を返すコードをここに実装
        // このコード内で必要な処理を行い、返り値を設定する

        // 例: 特定の値を返す
        return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"));
    }

    // トークンの転送関数
    function safeTransfer(address _to, uint256 _id, uint256 _amount, bytes memory _data)
        public
    {
        // 残高からトークンを減算
        balances[msg.sender][_id] -= _amount;
        
        // 受信者のアカウントに対してonERC1155Receivedコールバックを呼び出し
        bytes4 response = IERC1155Receiver(_to).onERC1155Received(msg.sender, address(this), _id, _amount, _data);
        require(response == bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")), "Invalid response");

        // 受信者のアカウントにトークンを追加
        balances[_to][_id] += _amount;
    }
}

// ERC-1155受信者のインターフェース
interface IERC1155Receiver {
    function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes memory _data)
        external returns(bytes4);
}

上記のコードは、ERC1155トークンの転送と受け取りコントラクトのコールバックの基本的な実装例です。onERC1155Received関数は、トークンが受け取りアドレスのコントラクトに転送される時に呼び出されます。 この関数内で特定の値を返すためのカスタムロジックを実装し、返り値を設定しています。 受け取りアドレスのコントラクトがこのコールバックを正しく処理することで、トークンの転送が行われます。

ただし、実際のコードにはセキュリティと最適化の考慮が必要です。 ERC1155規格はトークンの操作に関する複雑な側面を持つため、注意深く実装する必要があります。

また、複数のトークンを一括で操作するためのバッチコールも仕様に含まれています。 さらに、単一のオペレーターによる許可スキームは、コントラクト内の全てのトークンIDに無制限の許可を与えるものです。

ERC1155規格でのトークン許可の方法です。 あるユーザー(オペレーター)が、別のユーザーの代わりにトークンを転送できる権限を持つ場合、そのオペレーターはコントラクト内の全てのトークンIDに対して無制限の転送権限を持つことになります。 これは、オペレーターが複数のトークンを扱いやすくするための便利な方法ですが、権限の範囲が広すぎてセキュリティ上の懸念が生じる可能性もあります。

逆互換性は必要な箇所だけで意図的に削除されています。

ERC1155規格では、新しいバージョンへのアップデート時に、以前のバージョンとの互換性を維持することが必要な場面を選んで逆互換性を保持しています。 しかし、逆互換性の維持が不要な場合には、意図的に新しい機能や改善点を導入するために逆互換性を削除することもあります。 これにより、より新しいバージョンへのアップデートがスムーズに行えるようになります。

また、バッチコールや許可の増減メソッド、他のユーザーエクスペリエンスの向上に関する機能は、外部インターフェースを最小限に抑えるために意図的に仕様から省かれています。

ERC1155規格では、複数のトークンをまとめて操作するバッチコールや、トークンの許可を増減するためのメソッドなど、ユーザーエクスペリエンスを向上させるための機能が省略されています。 これは、外部インターフェースをシンプルに保つための選択肢です。 これにより、他の実装で異なる方法でこれらの機能を実現する余地が生まれ、異なる環境やニーズに合わせた柔軟な対応が可能になります。

ERC1155の仕様によれば、トークンの転送や複数のトークンのバッチ転送がコントラクトに対して行われる場合、コールバックが必要です。 この挙動は一部のケースでは望ましいかもしれませんが、*ERC721のようにtransferとsafeTransferの2つの方法が存在する場合、使用されるメソッドによってはコールバックが実行されないことがあります。 トークンコントラクト自体のランタイムパフォーマンスだけでなく、受け取りアドレスのコントラクトのランタイムパフォーマンスとコードサイズにも影響を及ぼし、複数のコールバック関数と返り値が必要になります。

トークンのバッチ転送は便利ですが、この規格では様々な実装において柔軟に転送操作を行えるようにするために機能を省いています。 例えば、コールデータのサイズ最適化やガス料金が高い環境のランタイムパフォーマンスなど、異なるABIエンコーディングを使用することで、各環境に最適な実装ができるようになります。

ハイブリッドな許可オペレータースキームは、トークンの許可を細かく設定できる一方で、スケーラビリティも考慮されています。 許可は、外部アカウントがユーザーのトークンIDごとにトークンを転送するための権限を与えるもので、オペレーターはユーザーの全てのトークンIDに対して完全な転送権限が与えられます。

ERC1155規格におけるハイブリッドな許可オペレータースキームは、トークンの許可を細かく設定する柔軟性と、同時にスケーラビリティ(拡張性)を考慮した設計です。

  1. 許可とオペレーターの役割

    • 許可
      • トークンの許可は、外部アカウントがユーザーの特定のトークンIDに対してトークンを転送するための権限を持つことを意味します。
      • これにより、ユーザーは自分が保有しているトークンの一部を他のアカウントに制御させることができます。
    • オペレーター
      • オペレーターは、権限を付与されたユーザーの全てのトークンIDに対して完全な転送権限を持ちます。
      • これによりオペレーターはユーザーのすべてのトークンを管理できるようになります。
  2. 許可オペレータースキームの特徴

    • 細かい設定と柔軟性
      • ユーザーは個別のトークンIDごとに許可を設定できるため、必要なトークンだけを特定のアカウントに転送させることができます。
      • これにより、トークンの細かい制御が可能です。
    • スケーラビリティ
      • 一方で、オペレーターはユーザーのすべてのトークンを管理するため、多数のトークンを持つ場合でもスケーラビリティを保つことができます。
      • 複数のトークンIDに対して一括で操作することができるため、効率的な管理が可能です。

仕様

定義

  • infinite
    • 整数を表す方法の一つであるuint256の最大値を指します。
    • uint256は256ビットの整数型で、その最大値は2の256乗から1を引いた値で、非常に大きな数値を表すのに使われます。
  • caller
    • ードが実行されている時点での操作を行ったアカウントを指します。
    • 例えば、コード内でトークンの転送が行われる場合、その操作を行ったアカウントがcallerとなります。
  • spender
    • あるアカウントが他のアカウントの代わりにトークンを転送する権限を持つことを意味します。
    • 具体的には、トークンの所有者が他のアカウントに許可を与え、そのアカウントが所有者のトークンを転送できるようにする際に使われる概念です。
  • operator
    • 特定のアカウントが他のアカウントの全てのトークンIDに対して無制限の転送権限を持つことを指します。
    • このアカウントは、所有者の許可なしでトークンを操作できるため、所有者が信頼するアカウントに与えられることがあります。
  • mint
    • 新しいトークンを作成することを指します。
    • トークンの供給を増やすために行われる操作で、通常は特別なメソッドやトランザクションを介して行われます。
  • burn
    • 既存のトークンを削除することを指します。
    • トークンの供給を減らすために行われる操作で、通常は特別なメソッドやトランザクションを介して行われます。

関数

totalSupply

yaml
- name: totalSupply
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
    - name: amount
      type: uint256

概要 特定のトークンIDの発行されているトークンの総量を取得する関数。

特定のトークンIDの保有者のbalanceOfの合計と等しくなる必要があります。

引数

  • id
    • 特定のトークンID。

戻り値

  • amount
    • 特定のトークンIDの発行されているトークンの総量。

balanceOf

yaml
- name: balanceOf
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address
    - name: id
      type: uint256

  outputs:
    - name: amount
      type: uint256

概要 特定のアドレスが保有している、特定のトークンIDのトークン総量を取得する関数。

引数

  • owner
    • トークン総量を確認したいアドレス。
  • id
    • 特定のトークンID。

戻り値

  • amount
    • 特定のアドレスが保有する特定のトークンIDの保有量。

allowance

yaml
- name: allowance
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address
    - name: spender
      type: address
    - name: id
      type: uint256

  outputs:
    - name: amount
      type: uint256

概要 特定のアドレスが保有する特定のトークンIDのトークンを、所有者に代わって送金できるトークン総量を取得する関数。

引数

  • owner
    • トークン所有者アドレス。
  • spender
    • トークン所有者アドレスの保有トークンのうち、どれだけ操作する許可を与えられているか確認するアドレス。
  • id
    • 特定のトークンID。

戻り値

  • amount
    • spenderが許可されているトークン総量。

isOperator

yaml
- name: isOperator
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address
    - name: spender
      type: address

  outputs:
    - name: status
      type: bool

概要 特定のアドレスが、トークン保有者のオペレーターとして承認されているか確認する関数。

引数

  • owner
    • トークン所有者アドレス。
  • spender
    • トークン保有者のオペレーターとして承認されているか確認したいアドレス。

戻り値

  • status
    • 承認されているかのbool値。

transfer

yaml
- name: transfer
  type: function
  stateMutability: nonpayable

  inputs:
    - name: receiver
      type: address
    - name: id
      type: uint256
    - name: amount
      type: uint256

  outputs:
    - name: success
      type: bool

概要 呼び出し元から指定されたアドレス(receiver)に対して、指定されたトークンIDのトークン量(amount)を転送する関数。

詳細 呼び出し元のアカウントから、指定されたアドレス(receiver)に対して、指定されたトークンIDのトークン量(amount)を転送します。 ただし、呼び出し元の残高が転送する量に足りない場合はエラーとなります。 転送が成功すると、Transferイベントがログに記録され、trueが返されます。

引数

  • receiver
    • トークンを受け取るアドレス。
  • id
    • 転送するトークンID。
  • amount
    • 転送する量。

戻り値

  • success
    • 転送が成功した場合はtrue。

transferFrom

yaml
- name: transferFrom
  type: function
  stateMutability: nonpayable

  inputs:
    - name: sender
      type: address
    - name: receiver
      type: address
    - name: id
      type: uint256
    - name: amount
      type: uint256

  outputs:
    - name: success
      type: bool

概要 呼び出し元が送信者(sender)から受信者(receiver)に対して、指定されたトークンIDのトークン量(amount)を転送する関数。

詳細 呼び出し元が送信者のオペレータであり、かつ送信者のトークンIDの許可されたトークン量が十分である場合、または全てのトークンが許可されている場合、指定された量のトークンを送信者から受信者に転送します。 転送が成功した場合、Transferイベントがログに記録され、trueが返されます。 また、送信者の残高が転送する量に足りない場合や許可されたトークン量十分でない場合、オペレータではない場合などの条件に該当する場合はエラーとなります。

引数

  • sender
    • トークンを送信するアドレス。
  • receiver
    • トークンを受け取るアドレス。
  • id
    • 転送するトークンの ID。
  • amount
    • 転送する量。

戻り値

  • success
    • 転送が成功した場合はtrue。

approve

yaml
- name: approve
  type: function
  stateMutability: nonpayable

  inputs:
    - name: spender
      type: address
    - name: id
      type: uint256
    - name: amount
      type: uint256

  outputs:
    - name: success
      type: bool

概要 指定されたアドレス(spender)が呼び出し元の代わりに、指定されたトークンIDのトークン量(amount)を転送できるように許可する関数。

詳細 指定されたアドレス(spender)に対して、指定されたトークンIDのallowanceを設定します。 このallowanceは、呼び出し元が指定されたアドレスに対して転送を行う時、指定されたトークン量までトークン保有者の代わりに転送できます。 allowanceが設定されると、Approvalイベントがログに記録され、trueが返されます。

引数

  • spender
    • トークンを転送するために許可されるアドレス(spender)。
  • id
    • allowanceを設定するトークンID。
  • amount
    • 許可するトークン量。

戻り値

  • success
    • 設定が成功した場合はtrue。

setOperator

javascript
function setOperator(address spender, bool approved) nonpayable returns (bool success)

概要 呼び出し元が指定されたアドレス(spender)に対して、任意のトークンIDに対する無制限の転送権限を許可または拒否する関数。

詳細 指定されたアドレス(spender)に対して、無制限のトークン転送権限(オペレータ権限)を付与または取り消します。 オペレータ権限が設定されると、OperatorSetイベントがログに記録され、trueが返されます。

引数

  • spender
    • オペレータ権限を設定するアドレス。
  • approved
    • オペレータ権限を許可する場合はtrue、取り消す場合はfalse。

戻り値

  • success
    • 設定が成功した場合はtrue。

イベント

もかかりました。以下に各変数やイベント、構造体、修飾子の説明を示します。


Transfer

yaml
- name: Transfer
  type: event

  inputs:
    - name: sender
      indexed: true
      type: address
    - name: receiver
      indexed: true
      type: address
    - name: id
      indexed: true
      type: uint256
    - name: amount
      indexed: false
      type: uint256

概要 特定のトークンIDのトークン量が1つのアカウントから別のアカウントに転送された時に発行されるイベント。

詳細 トークンがmintされた場合は、送信者のアドレスはゼロアドレスとして記録されます。 トークンがバーンされた場合は、受信者のアドレスがゼロアドレスとして記録されます。

パラメータ

  • sender
    • トークンを送信したアドレス。
  • receiver
    • トークンを受け取ったアドレス。
  • id
    • 転送されたトークンID。
  • amount
    • 転送されたトークン量。

OperatorSet

yaml
- name: OperatorSet
  type: event

  inputs:
    - name: owner
      indexed: true
      type: address
    - name: spender
      indexed: true
      type: address
    - name: approved
      indexed: false
      type: bool

概要 オーナーが特定のアドレス(spender)のオペレータ権限の状態を設定した時に発行されるイベント。

パラメータ

  • owner
    • オペレータ権限を設定したオーナーのアドレス。
  • spender
    • オペレータ権限が設定されたアドレス。
  • approved
    • オペレータ権限が設定された状態(trueまたはfalse)。

Approval

yaml
- name: Approval
  type: event

  inputs:
    - name: owner
      indexed: true
      type: address
    - name: spender
      indexed: true
      type: address
    - name: id
      indexed: true
      type: uint256
    - name: amount
      indexed: false
      type: uint256

概要 オーナーが特定のアドレス(spender)のトークンIDに対するallowanceを設定した時に発行されるイベント。

パラメータ

  • owner
    • allowanceを設定したオーナーのアドレス。
  • spender
    • allowanceが設定されたアドレス。
  • id
    • allowanceが設定されたトークンID。
  • amount
    • 設定されたallowanceのトークン量。

補足

グラニュラーな承認

ERC1155標準のオペレーターモデルでは、アカウントが他のアカウントをオペレーターとして設定することで、所有者の代わりに任意のトークンIDの任意のトークン量を転送する権限を付与できます。 しかし、この方法は常に適切な権限の設定とは限りません。 一方、ERC20標準のallowanceモデルでは、アカウントが他のアカウントに所有者の代わりに使用できるトークンの明示的な数量を設定できます。 この標準では、両方のモデルを実装しトークンIDも指定する必要があります。 これにより、特定のトークンIDに対する特定の承認、特定のトークンIDに対する無制限の承認、あるいはすべてのトークンIDに対する無制限の承認を与えることができます。 ただし、アカウントがオペレーターとして設定されている場合、オーナーの代わりにトークンが転送される際にallowanceは減少しないように注意する必要があります。

バッチ処理の削除

バッチ処理は便利ですが、標準自体に含めるべきではなく、状況に応じて個別に考えるべきです。 これにより、calldataのレイアウトに関する異なる選択が可能になります。 特に、calldataをグローバルストレージにコミットするロールアップなどの特定のアプリケーションにとっては、選択肢の幅が広がります。

必須コールバックの削除

コールバックは、複数のトークンに対応したコントラクト内で使用することができますが、必須ではありません。 これにより、外部呼び出しと追加のチェックが削減され、ガスの効率的な利用が可能になります。

safeの削除

safeTransferとsafeTransferFromの命名規則は、特にERC1155やERC721の標準の文脈では、誤解を招くことがあります。 これらのメソッドは、コードを持つ受信者アカウントに対して外部呼び出しを行う必要があります。 受信者のコントラクトが特定の条件を満たすことを前提として、コントロールフローが任意のコントラクトに移行します。 必須のコールバックを削除し、すべてのメソッド名から「safe」の単語を取り除くことで、コントロールフローの安全性がデフォルトで向上します。

safeTransferとsafeTransferFromの命名規則の誤解

あるゲーム内でトークンを転送する場面を考えてみます。 プレイヤーAがプレイヤーBにトークンを転送したいとします。 このとき、safeTransferメソッドを呼び出します。 しかし、この名前からは「安全なトランスファー」という意味が伝わりますが、実際にはプレイヤーBの受け取りアドレスのコントラクトが特定の条件を満たすかどうかによって、コントロールフローが外部のコントラクトに移行する可能性があります。

例1: エラトステネスの篩コントラクト

受け取りアドレスコントラクトがエラトステネスの篩アルゴリズムを実装していると考えてみましょう。safeTransferメソッドを呼び出すと、プレイヤーBのコントラクトは数学的な計算を実行し、特定の条件を満たすかどうかを判断します。 条件に適合すれば、トークンが転送されますが、そうでなければトランザクションは失敗します。

https://algo-method.com/descriptions/64

解決策: コントロールフローの安全性向上

これらの誤解と混乱を避けるために、safeTransferとsafeTransferFromのような名前が使用される代わりに、より明確な名前が提案されています。 また、コントロールフローが外部のコントラクトに移行する可能性があるため、コールバックは必須ではなく、コントロールフローの安全性を高めるために外部コールバックの要件を削除することが提案されています。 これにより、デフォルトでトランザクションの安全性が向上し、開発者が予測可能な動作を持つコードを作成できるようになります。

このように、safeTransferとsafeTransferFromの命名規則や外部コールバックの要件が、コントロールフローの安全性と開発の予測可能性に影響を与えることがわかります。

インターフェースID

インターフェースID は、異なるコントラクト間で特定のインターフェースを識別するための一意の識別子です。 この規格においては、特定のインターフェースを指し示すために0xb2e69f8aという値が使用されます。

メタデータ

name

コントラクトの名前。

yaml
- name: name
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: name
      type: string

symbol

コントラクトのシンボル。

yaml
- name: symbol
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: symbol
      type: string

decimals

トークンの小数点以下の桁数。

yaml
- name: decimals
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
  - name: amount
    type: uint8

メタデータURI拡張

tokenURI

トークンIDのURI。 クライアントは、返されたURI文字列中の{id}の部分を置換する。

yaml
- name: tokenURI
  type: function
  stateMutability: view

  inputs:
    - name: id
      type: uint256

  outputs:
    - name: uri
      type: string

メタデータの構造

メタデータの仕様は、ERC721のJSONスキーマに従っている。 クライアントは、返されたURI文字列中の{id}の出現を置換する。

json
{
  "title": "Asset Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the token"
    },
    "description": {
      "type": "string",
      "description": "Describes the token"
    },
    "image": {
      "type": "string",
      "description": "A URI pointing to an image resource."
    }
  }
}

後方互換性

新しい仕様の導入により、ERC1155との後方互換性は失われる可能性があります。 なぜなら、一部のメソッドが削除されるためです。 しかし、既存の標準である ERC20、ERC721、ERC1155に対するラッパー(Wrapper)を実装することで、後方互換性を保つことができます。

参考実装

javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.19;

/// @title ERC6909 Multi-Token Reference Implementation
/// @author jtriley.eth
contract ERC6909 {
    /// @dev Thrown when owner balance for id is insufficient.
    /// @param owner The address of the owner.
    /// @param id The id of the token.
    error InsufficientBalance(address owner, uint256 id);

    /// @dev Thrown when spender allowance for id is insufficient.
    /// @param spender The address of the spender.
    /// @param id The id of the token.
    error InsufficientPermission(address spender, uint256 id);

    /// @notice The event emitted when a transfer occurs.
    /// @param sender The address of the sender.
    /// @param receiver The address of the receiver.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    event Transfer(address indexed sender, address indexed receiver, uint256 indexed id, uint256 amount);

    /// @notice The event emitted when an operator is set.
    /// @param owner The address of the owner.
    /// @param spender The address of the spender.
    /// @param approved The approval status.
    event OperatorSet(address indexed owner, address indexed spender, bool approved);

    /// @notice The event emitted when an approval occurs.
    /// @param owner The address of the owner.
    /// @param spender The address of the spender.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount);

    /// @notice The total supply of each id.
    mapping(uint256 id => uint256 amount) public totalSupply;

    /// @notice Owner balance of an id.
    mapping(address owner => mapping(uint256 id => uint256 amount)) public balanceOf;

    /// @notice Spender allowance of an id.
    mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) public allowance;

    /// @notice Checks if a spender is approved by an owner as an operator.
    mapping(address owner => mapping(address spender => bool)) public isOperator;

    /// @notice Transfers an amount of an id from the caller to a receiver.
    /// @param receiver The address of the receiver.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    function transfer(address receiver, uint256 id, uint256 amount) public returns (bool) {
        if (balanceOf[msg.sender][id] < amount) revert InsufficientBalance(msg.sender, id);
        balanceOf[msg.sender][id] -= amount;
        balanceOf[receiver][id] += amount;
        emit Transfer(msg.sender, receiver, id, amount);
        return true;
    }

    /// @notice Transfers an amount of an id from a sender to a receiver.
    /// @param sender The address of the sender.
    /// @param receiver The address of the receiver.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public returns (bool) {
        if (sender != msg.sender && !isOperator[sender][msg.sender]) {
            uint256 senderAllowance = allowance[sender][msg.sender][id];
            if (senderAllowance < amount) revert InsufficientPermission(msg.sender, id);
            if (senderAllowance != type(uint256).max) {
                allowance[sender][msg.sender][id] = senderAllowance - amount;
            }
        }
        if (balanceOf[sender][id] < amount) revert InsufficientBalance(sender, id);
        balanceOf[sender][id] -= amount;
        balanceOf[receiver][id] += amount;
        emit Transfer(sender, receiver, id, amount);
        return true;
    }

    /// @notice Approves an amount of an id to a spender.
    /// @param spender The address of the spender.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    function approve(address spender, uint256 id, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender][id] = amount;
        emit Approval(msg.sender, spender, id, amount);
        return true;
    }


    /// @notice Sets or removes a spender as an operator for the caller.
    /// @param spender The address of the spender.
    /// @param approved The approval status.
    function setOperator(address spender, bool approved) public returns (bool) {
        isOperator[msg.sender][spender] = approved;
        emit OperatorSet(msg.sender, spender, approved);
        return true;
    }

    /// @notice Checks if a contract implements an interface.
    /// @param interfaceId The interface identifier, as specified in ERC-165.
    /// @return supported True if the contract implements `interfaceId`.
    function supportsInterface(bytes4 interfaceId) public pure returns (bool supported) {
        return interfaceId == 0xb2e69f8a || interfaceId == 0x01ffc9a7;
    }

    function _mint(address receiver, uint256 id, uint256 amount) internal {
      // WARNING: important safety checks should precede calls to this method.
      balanceOf[receiver][id] += amount;
      totalSupply[id] += amount;
      emit Transfer(address(0), receiver, id, amount);
    }

    function _burn(address sender, uint256 id, uint256 amount) internal {
      // WARNING: important safety checks should precede calls to this method.
      balanceOf[sender][id] -= amount;
      totalSupply[id] -= amount;
      emit Transfer(sender, address(0), id, amount);
    }
}

セキュリティ考慮事項

許可の委任モデルには、「allowance(許可)」モデルと「operator(オペレーター)」モデルの2つが含まれています。 以下に、これらの許可モデルに関する2つのセキュリティに関する考慮事項について、わかりやすく説明します。

  1. 許可モデルのセキュリティ考慮事項:

許可モデルでは、特定のアカウントに対してトークンの送信許可を委任します。 この許可が有効である限り、そのアカウントはいつでも全額のトークンを送信できます。 例えば、トークン所有者が別のアカウントに対して一定量のトークン送信を許可した場合、許可が取り消されるまで、そのアカウントはその許可額いっぱいまでトークンを送信できます。

  1. オペレーターモデルのセキュリティ考慮事項:

オペレーターモデルでは、トークン所有者は他のアカウントに対して、特定のトークンを送信する権限を付与できます。 オペレーター権限を持つアカウントは、所有者の代わりにトークンを送信できるため、トークンの流通を効率的に行うのに役立ちます。

  1. 両方の委任許可モデルの組み合わせに関するユニークなセキュリティ考慮事項:

transferFromメソッドに従い、オペレーター権限を持つアドレスは許可制限の対象外です。 無限の承認を持つアドレスは、トークンの送信時に許可額が減少してはならず、無限の承認を持たないアドレスは、トークン送信時にそのバランスが減少する必要があります。 ただし、オペレーター権限と非無限承認の両方を持つ支出者は、機能的な曖昧さを導入する可能性があります。

オペレーター権限が優先される場合、つまり、アドレスがオペレーター権限を持っている場合許可額は減少しません。しかし、許可額がオペレーター権限よりも優先される場合、許可額のアンダーフローが発生しないようにするために、追加の条件分岐が必要になる場合があります。

以下のコード例は、この問題を示しています。 ERC6909OperatorPrecedenceとERC6909AllowancePrecedenceの2つのコントラクトは、オペレーター権限が優先される場合と許可額が優先される場合を示しています。 許可額のアンダーフローを防ぐためには、適切な条件分岐とエラーハンドリングが必要です。

javascript
contract ERC6909OperatorPrecedence {
  // -- snip --

  function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public {
    // check if `isOperator` first
    if (msg.sender != sender && !isOperator[sender][msg.sender]) {
      require(allowance[sender][msg.sender][id] >= amount, "insufficient allowance");
      allowance[sender][msg.sender][id] -= amount;
    }
  
    // -- snip --
  }
}

contract ERC6909AllowancePrecedence {
  // -- snip --

  function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public {
    // check if allowance is sufficient first
    if (msg.sender != sender && allowance[sender][msg.sender][id] < amount) {
      require(isOperator[sender][msg.sender], "insufficient allowance");
    }

    // ERROR: when allowance is insufficient, this panics due to arithmetic underflow, regardless of
    // whether the caller has operator permissions.
    allowance[sender][msg.sender][id] -= amount;

    // -- snip
  }
}

引用

Joshua Trujillo (@jtriley-eth), "ERC-6909: Minimal Multi-Token Interface [DRAFT]," Ethereum Improvement Proposals, no. 6909, April 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6909.

最後に

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

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