はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、所有者を変更せずにNFTの利用権だけを別のユーザーに委ねられる仕組みを提案しているERC7695についてまとめていきます!

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC7695は、ERC721を拡張して「誰がそのNFTを使えるのか」を状況に応じて明確にし、さらにロック(lock)機能を持たせるための標準です。 ここで言うロックは、NFTをどこかに預けて動かせない状態にするだけでなく、「勝手に移転できない状態にしつつ使う権利は一時的に渡せる」ことが重要です。

ERC721については以下の記事を参考にしてください。

従来のERC721は、基本的に「Owner(所有者)」が唯一の中心的な役割でした。 OwnerはNFTを移転でき、所有者であること自体が多くのdAppsで「特典を受ける資格」の根拠になります。 一方で、金融系ユースケース(ステーキング、担保、レンタルなど)では、NFTをロックするためにコントラクトへ移転してしまうことが多く、これが別のdApps側の「所有している人向け特典」と衝突します。

ERC7695はこの問題を、Ownership Delegation(所有権の委任)と Contexts(コンテキスト)という考え方で整理します。

  • Ownership Delegation(所有権の委任) 元のOwnerを変えずに、NFTを使う権利を一時的に別の人へ渡せる仕組みです。 「所有者はそのまま、利用者だけが変わる」を標準として扱います。
  • Contexts(コンテキスト) 「どの用途・場面で使う権利か」を切り分ける枠組みです。 例えば、同じNFTでも「ゲーム内で装備として使う」、「投票に使う」、「レンタルとして使う」といった文脈が異なります。 ERC7695は、この文脈ごとに誰が使えるかを定義できるようにします。

その上で、ERC7695は役割を以下のように整理します。

役割 意味 できることのイメージ
Owner(所有者) NFTの最終的な権利者 基本的な所有権・利益(特典など)を保持する前提
Controller(コントローラ) コンテキスト内で管理する役割 ある用途における「使う人」の指定や運用をコントロールする役割
User(ユーザー) 実際にNFTを利用する役割 コンテキストで定義された範囲でNFTを使える

Controllerは「その用途の運用責任者」です。 例えばレンタル用途なら、レンタル契約を管理する主体(コントラクトや運営者など)がControllerになり、借り手をUserとして設定します。 Contextsは「用途ごとに権利を分ける箱」です。 箱が違えば、同じNFTでも誰が使えるかを別々に扱えます。

ERC7695の狙いは、Ownerの利益や権利を守ったまま、NFTの利用価値を複数のdAppsにまたがって広げることです。 OwnerはOwnerのままなので、所有者向けの権利(例: 保有者特典)を失いにくく、同時に別の人へ一時的な利用権を渡すことができます。

動機

ERC721のNFTは、金融アプリケーションで以下のような用途があります。

用途 何をしたいか 従来よくあるやり方
ステーキング NFTを預けて報酬を得る NFTをステーキング用コントラクトへ移転してロック
担保(モーゲージ) NFTを担保に収益や資金を得る 担保コントラクトへ移転して拘束
レンタル・委任 他人に使わせて対価を得る(利用権の貸し出し等) 一時的に所有権を渡す、またはコントラクトが保管して利用を代行

ここで問題になるのは、「ロックするために所有権移転が必要になりがち」な点です。 従来の設計では、NFTをコントラクトに預けて移転してしまうと、ブロックチェーン上のOwnerはそのコントラクトになります。 その結果、別のdAppsが「Ownerであること」を特典付与の条件にしている場合、本来の持ち主が特典を受けられなくなります。

例えば以下のようなケースです。

  • あるNFTを持っていると、別のプロジェクトでエアドロップ(無料配布のトークン)を受け取れる設計になっている
  • しかし、そのNFTをステーキングや担保目的でコントラクトに預けると、Ownerはコントラクトに変わる
  • すると、エアドロップは「コントラクト」に対して送られるか、そもそも受け取り対象外になる
  • 本来の保有者は、NFTを活用したいのに保有者特典を失う

この問題を回避するには、特典を提供するdApps側が「ステーキングコントラクトや担保コントラクトをできるだけ多く認識して、預けた先のコントラクト経由でも元の持ち主へ配る」ような対応をする必要が出ます。 ただし、これは現実的に難しくなりがちです。 コントラクトは無数にあり、どれを正として扱うか、どの仕様に対応するか、更新をどう追従するかがdApps側の負担になります。

この背景には、「Ownerという役割しか標準化されていない」ことがあります。ERC721の上に何かを作る場合、所有権以外の役割(利用者、管理者など)はプロトコルごとに独自に定義されます。 そのため、あるdAppでは預け先コントラクトをOwnerとして扱うし、別のdAppでは本来の持ち主を救済する独自ロジックを持つなど、互換性が崩れやすくなります。

ERC7695は、この難しさを「コンテキストでユースケースを整理し、コントローラが管理し、ユーザーが利用する」という形で標準レベルに持ち上げます。 所有権(Owner)と、それ以外の役割を分離し、Ownership Delegationで“利用権を一時的に渡す”ことを正面から扱います。

その結果、dApps開発者は次のようなメリットを得ます。

  • 「所有者は誰か」と「誰が使えるか」を同じ概念で無理に扱わずに済みます。 Ownerは特典や権利の基準として保ちつつ、利用はUserに任せられます。
  • どのプロトコルでも同じ考え方(Controller / User / Contexts)で扱えるため、インフラや周辺ツール(インデクサ、ウォレット表示、権限確認など)を作りやすくなります。
  • NFTを活用しても、Ownerとしての立場を保ちやすくなり、保有者向けの仕組みと金融ユースケースが衝突しにくくなります。

コンセプトの関係図

flowchart LR
  O[Owner<br/>所有者] -->|Ownership Delegation<br/>所有権は維持したまま権利を委任| C[Controller<br/>コンテキスト内の管理者]
  C -->|ContextごとにUserを指定| U[User<br/>実利用者]
  subgraph X[Contexts(用途の箱)]
    X1[例: レンタル]
    X2[例: ステーキング利用]
    X3[例: ゲーム内利用]
  end
  C --> X
  U --> X

この図のポイントは、Ownerが変わらずに残りつつ、コンテキスト(用途)ごとにControllerがUser(使う人)を指定できる、という構造です。 Ownerの権利を守りながら、NFTの使い道を広げるための骨格になっています。

仕様

定義

トークンコンテキスト

トークンコンテキストは、「そのNFTをどの用途で使うか」を明示するための概念です。 NFT(トークン)とコンテキストを結びつける関係そのものを指します。

1つのトークンコンテキストごとに、「その用途で使うことを許可されたユーザー」が必ず1人割り当てられます。 つまり、同じNFTでも、用途が違えば別のユーザーが使える状態を同時に持てます。

各コンテキストの中には、以下の2つの役割があります。

  • Controller そのコンテキストを管理する立場です。誰を使える状態にするか、ロックするかといった操作権限を持ちます。
  • User そのコンテキストにおいて、実際にNFTを利用できる主体です。

ここで重要なのは、「NFT全体の所有者」と「コンテキスト内で使う人」は切り離されている点です。

所有権の権利

ERC7695では、Ownerという立場そのものではなく、「所有権に含まれる具体的な権利」を明確に定義しています。 トークンの所有権が持つ権利は、以下の操作を行えることです。

  • トークンを新しい所有者へ移転すること
  • トークンを1つまたは複数のコンテキストに紐づけること
  • トークンを1つまたは複数のコンテキストから外すこと

これらがまとめて「Ownership Rights(所有権の権利)」として扱われます。 単に「Ownerである」という状態ではなく、「何ができるのか」をはっきり定義している点が特徴です。

所有権の委任

所有権の委任は、「Owner」と「Ownership Rights(所有権の権利)」を分離する仕組みです。

Ownerはトークンの最終的な所有者ですが、一定期間だけ別のアカウントに所有権の権利を委ねることができます。 この委任には有効期限があります。

委任期間中は、Owner自身は所有権の権利を行使できません。 委任が期限切れになると、自動的にOwnerへ権利が戻ります。

この仕組みによって、所有者は変えずに管理や運用に必要な権限だけを一時的に他者へ渡すという運用が可能になります。

ロール

ERC7695では、トークンに関わる役割を明確に分けています。 以下の表は、それぞれの役割と権限、トークンあたりの数を整理したものです。

トークン全体に関わるロール

ロール名 権限・役割の説明 トークンあたりの数
Owner デフォルトで所有権の権利を持つ。一定期間、別のアカウントに所有権の権利を委任できる。 1
Ownership Delegatee 委任期間中に所有権の権利を持つ。期限前に権利を放棄することもできる。 最大1
Ownership Manager 現時点で所有権の権利を保持している主体。委任がなければOwner、委任中ならOwnership Delegateeを指す。 1

Ownership Managerは、「今、実際に所有権の操作をできるのは誰か」を指すための概念です。 OwnerかDelegateeかを毎回判定せずに済むようにするための抽象的な役割です。

コンテキストごとのロール

ロール名 権限・役割の説明 数
Controller コンテキスト内の管理者。Controllerの移転、Userの設定、トークン移転のロック・解除を行える。 コンテキストごとに1
User そのコンテキスト内でNFTの利用を許可された主体。 コンテキストごとに1

Controllerは、「その用途における責任者」です。 Userは「実際に使う人」であり、管理権限は持ちません。

ロールと権限の関係図

文章だけでは少し分かりにくいため、全体の関係を図にします。

flowchart LR
  O[Owner<br/>最終的な所有者] -->|所有権の委任| D[Ownership Delegatee]
  O --> M[Ownership Manager]
  D --> M

  M -->|コンテキストを追加・削除| Ctx[Token Context]

  Ctx --> C[Controller]
  C -->|Userを設定| U[User]

  C -->|移転ロック制御| T[NFT Transfer]

この構造により、以下の役割分担が明確になります。

  • Ownerは変わらずに存在し続ける
  • 所有権の操作はOwnership Managerが担う
  • 用途ごとの管理はControllerが行う
  • 実際の利用はUserが行う

インターフェース

IERC7695.sol
/// Note: the ERC-165 identifier for this interface is 0x486b6fba.
interface IERC7695 /* is IERC721, IERC165 */ {
  /// @dev This emits when a context is updated by any mechanism.
  event ContextUpdated(bytes32 indexed ctxHash, address indexed controller, uint64 detachingDuration);
  /// @dev This emits when a token is attached to a certain context by any mechanism.
  event ContextAttached(bytes32 indexed ctxHash, uint256 indexed tokenId);
  /// @dev This emits when a token is requested to detach from a certain context by any mechanism.
  event ContextDetachmentRequested(bytes32 indexed ctxHash, uint256 indexed tokenId);
  /// @dev This emits when a token is detached from a certain context by any mechanism.
  event ContextDetached(bytes32 indexed ctxHash, uint256 indexed tokenId);
  /// @dev This emits when a user is assigned to a certain context by any mechanism.
  event ContextUserAssigned(bytes32 indexed ctxHash, uint256 indexed tokenId, address indexed user);
  /// @dev This emits when a token is (un)locked in a certain context by any mechanism.
  event ContextLockUpdated(bytes32 indexed ctxHash, uint256 indexed tokenId, bool locked);
  /// @dev This emits when the ownership delegation is started by any mechanism.
  event OwnershipDelegationStarted(uint256 indexed tokenId, address indexed delegatee, uint64 until);
  /// @dev This emits when the ownership delegation is accepted by any mechanism.
  event OwnershipDelegationAccepted(uint256 indexed tokenId, address indexed delegatee, uint64 until);
  /// @dev This emits when the ownership delegation is stopped by any mechanism.
  event OwnershipDelegationStopped(uint256 indexed tokenId, address indexed delegatee);

  /// @notice Gets the longest duration the detaching can happen.
  function maxDetachingDuration() external view returns (uint64);

  /// @notice Gets controller address and detachment duration of a context.
  /// @dev MUST revert if the context is not existent.
  /// @param ctxHash            A hash of context to query the controller.
  /// @return controller        The address of the context controller.
  /// @return detachingDuration The duration must be waited for detachment in second(s).
  function getContext(bytes32 ctxHash) external view returns (address controller, uint64 detachingDuration);

  /// @notice Creates a new context.
  /// @dev MUST revert if the context is already existent.
  /// MUST revert if the controller address is zero address.
  /// MUST revert if the detaching duration is larger than max detaching duration.
  /// MUST emit the event {ContextUpdated} to reflect context created and controller set.
  /// @param controller        The address that controls the created context.
  /// @param detachingDuration The duration must be waited for detachment in second(s).
  /// @param ctxMsg            The message of new context to be used for hashing.
  /// @return ctxHash          Hash of the created context.
  function createContext(address controller, uint64 detachingDuration, bytes calldata ctxMsg)
    external
    returns (bytes32 ctxHash);

  /// @notice Updates an existing context.
  /// @dev MUST revert if method caller is not the current controller.
  /// MUST revert if the context is non-existent.
  /// MUST revert if the new controller address is zero address.
  /// MUST revert if the detaching duration is larger than max detaching duration.
  /// MUST emit the event {ContextUpdated} on success.
  /// @param ctxHash              Hash of the context to set.
  /// @param newController        The address of new controller.
  /// @param newDetachingDuration The new duration must be waited for detachment in second(s).
  function updateContext(bytes32 ctxHash, address newController, uint64 newDetachingDuration) external;

  /// @notice Queries if a token is attached to a certain context.
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to query.
  /// @return        True if the token is attached to the context, false if not.
  function isAttachedWithContext(bytes32 ctxHash, uint256 tokenId) external view returns (bool);

  /// @notice Attaches a token with a certain context.
  /// @dev See "attachContext rules" in "Token (Un)lock Rules".
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to be attached.
  /// @param data    Additional data with no specified format, MUST be sent unaltered in call to the {IERC7695ContextCallback} hook(s) on controller.
  function attachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;

  /// @notice Requests to detach a token from a certain context.
  /// @dev See "requestDetachContext rules" in "Token (Un)lock Rules".
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to be detached.
  /// @param data    Additional data with no specified format, MUST be sent unaltered in call to the {IERC7695ContextCallback} hook(s) on controller.
  function requestDetachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;

  /// @notice Executes context detachment.
  /// @dev See "execDetachContext rules" in "Token (Un)lock Rules".
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to be detached.
  /// @param data    Additional data with no specified format, MUST be sent unaltered in call to the {IERC7695ContextCallback} hook(s) on controller.
  function execDetachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;

  /// @notice Finds the context user of a token.
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to be detached.
  /// @return user   Address of the context user.
  function getContextUser(bytes32 ctxHash, uint256 tokenId) external view returns (address user);

  /// @notice Updates the context user of a token.
  /// @dev MUST revert if the method caller is not context controller.
  /// MUST revert if the context is non-existent.
  /// MUST revert if the token is not attached to the context.
  /// MUST emit the event {ContextUserAssigned} on success.
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to be update.
  /// @param user    Address of the new user.
  function setContextUser(bytes32 ctxHash, uint256 tokenId, address user) external;

  /// @notice Queries if the lock a token is locked in a certain context.
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to be queried.
  /// @return        True if the token context is locked, false if not.
  function isTokenContextLocked(bytes32 ctxHash, uint256 tokenId) external view returns (bool);

  /// @notice (Un)locks a token in a certain context.
  /// @dev See "setContextLock rules" in "Token (Un)lock Rules".
  /// @param ctxHash Hash of a context.
  /// @param tokenId The NFT to be queried.
  /// @param lock    New status to be (un)locked.
  function setContextLock(bytes32 ctxHash, uint256 tokenId, bool lock) external;

  /// @notice Finds the ownership manager of a specified token.
  /// @param tokenId  The NFT to be queried.
  /// @return manager Address of delegatee.
  function getOwnershipManager(uint256 tokenId) external view returns(address manager);

  /// @notice Finds the ownership delegatee of a token.
  /// @dev MUST revert if there is no (or an expired) ownership delegation.
  /// @param tokenId    The NFT to be queried.
  /// @return delegatee Address of delegatee.
  /// @return until     The delegation expiry time.
  function getOwnershipDelegatee(uint256 tokenId) external view returns (address delegatee, uint64 until);

  /// @notice Finds the pending ownership delegatee of a token.
  /// @dev MUST revert if there is no (or an expired) pending ownership delegation.
  /// @param tokenId    The NFT to be queried.
  /// @return delegatee Address of pending delegatee.
  /// @return until     The delegation expiry time in the future.
  function pendingOwnershipDelegatee(uint256 tokenId) external view returns (address delegatee, uint64 until);

  /// @notice Starts ownership delegation and retains ownership until a specific timestamp.
  /// @dev Replaces the pending delegation if any.
  /// See "startDelegateOwnership rules" in "Ownership Delegation Rules".
  /// @param tokenId   The NFT to be delegated.
  /// @param delegatee Address of new delegatee.
  /// @param until     The delegation expiry time.
  function startDelegateOwnership(uint256 tokenId, address delegatee, uint64 until) external;

  /// @notice Accepts ownership delegation request.
  /// @dev See "acceptOwnershipDelegation rules" in "Ownership Delegation Rules".
  /// @param tokenId The NFT to be accepted.
  function acceptOwnershipDelegation(uint256 tokenId) external;

  /// @notice Stops the current ownership delegation.
  /// @dev See "stopOwnershipDelegation rules" in "Ownership Delegation Rules".
  /// @param tokenId The NFT to be stopped.
  function stopOwnershipDelegation(uint256 tokenId) external;
}

了解しました。言葉は強いですが、やることは明確です。 以下は、提示された ERC7695 のコード要素(イベント/関数)を、あなたのテンプレートどおりに 出力し直したものです。 Ownership Delegation Rules / Token (Un)lock Rules / Additional Transfer Rules も、該当する関数の 詳細 に必ず反映しています。

javascript
// Note: ERC-165 identifier for IERC7695 is 0x486b6fba.
interface IERC7695 /* is IERC721, IERC165 */ {
  event ContextUpdated(bytes32 indexed ctxHash, address indexed controller, uint64 detachingDuration);
  event ContextAttached(bytes32 indexed ctxHash, uint256 indexed tokenId);
  event ContextDetachmentRequested(bytes32 indexed ctxHash, uint256 indexed tokenId);
  event ContextDetached(bytes32 indexed ctxHash, uint256 indexed tokenId);
  event ContextUserAssigned(bytes32 indexed ctxHash, uint256 indexed tokenId, address indexed user);
  event ContextLockUpdated(bytes32 indexed ctxHash, uint256 indexed tokenId, bool locked);
  event OwnershipDelegationStarted(uint256 indexed tokenId, address indexed delegatee, uint64 until);
  event OwnershipDelegationAccepted(uint256 indexed tokenId, address indexed delegatee, uint64 until);
  event OwnershipDelegationStopped(uint256 indexed tokenId, address indexed delegatee);

  function maxDetachingDuration() external view returns (uint64);
  function getContext(bytes32 ctxHash) external view returns (address controller, uint64 detachingDuration);
  function createContext(address controller, uint64 detachingDuration, bytes calldata ctxMsg) external returns (bytes32 ctxHash);
  function updateContext(bytes32 ctxHash, address newController, uint64 newDetachingDuration) external;
  function isAttachedWithContext(bytes32 ctxHash, uint256 tokenId) external view returns (bool);
  function attachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;
  function requestDetachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;
  function execDetachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;
  function getContextUser(bytes32 ctxHash, uint256 tokenId) external view returns (address user);
  function setContextUser(bytes32 ctxHash, uint256 tokenId, address user) external;
  function isTokenContextLocked(bytes32 ctxHash, uint256 tokenId) external view returns (bool);
  function setContextLock(bytes32 ctxHash, uint256 tokenId, bool lock) external;
  function getOwnershipManager(uint256 tokenId) external view returns(address manager);
  function getOwnershipDelegatee(uint256 tokenId) external view returns (address delegatee, uint64 until);
  function pendingOwnershipDelegatee(uint256 tokenId) external view returns (address delegatee, uint64 until);
  function startDelegateOwnership(uint256 tokenId, address delegatee, uint64 until) external;
  function acceptOwnershipDelegation(uint256 tokenId) external;
  function stopOwnershipDelegation(uint256 tokenId) external;
}

// Note: ERC-165 identifier for IERC7695Enumerable is 0xcebf44b7.
interface IERC7695Enumerable /* is IERC165 */ {
  function getContext(uint256 index) external view returns(bytes32 ctxHash);
  function getContextCount() external view returns(uint256);
  function getAttachedContext(uint256 tokenId, uint256 index) external view returns(bytes32 ctxHash);
  function getAttachedContextCount(uint256 tokenId) external view returns(uint256);
}

// Note: ERC-165 identifier for IERC7695ContextCallback is 0xad0491f1.
interface IERC7695ContextCallback /* is IERC165 */  {
  function onAttached(bytes32 ctxHash, uint256 tokenId, address operator, bytes calldata data) external;
  function onDetachRequested(bytes32 ctxHash, uint256 tokenId, address operator, bytes calldata data) external;
  function onExecDetachContext(bytes32 ctxHash, uint256 tokenId, address user, address operator, bytes calldata data) external;
}

イベント

ContextUpdated
javascript
event ContextUpdated(bytes32 indexed ctxHash, address indexed controller, uint64 detachingDuration);

コンテキストが作成または更新された時に発行されるイベント。 コンテキストの新規作成や更新が起きたことを通知するイベントです。controller と detachingDuration の現在値を追跡できます。

パラメータ

  • ctxHash
    • 対象コンテキストを識別するハッシュ。
  • controller
    • コンテキストを管理するコントローラのアドレス。
  • detachingDuration
    • デタッチ実行までに待つ秒数。
ContextAttached
javascript
event ContextAttached(bytes32 indexed ctxHash, uint256 indexed tokenId);

トークンがコンテキストにアタッチされた時に発行されるイベント。 NFTが指定コンテキストに紐づいたことを示します。 以後、そのコンテキストでユーザー設定やロック制御の対象になります。

パラメータ

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
ContextDetachmentRequested
javascript
event ContextDetachmentRequested(bytes32 indexed ctxHash, uint256 indexed tokenId);

ロック中のトークンについてデタッチ要求が行われた時に発行されるイベント。 トークンコンテキストがロックされているため即時デタッチできず、「待機状態」に入ったことを示します。 detachingDuration 経過後に execDetachContext で実デタッチできます。

パラメータ

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
ContextDetached
javascript
event ContextDetached(bytes32 indexed ctxHash, uint256 indexed tokenId);

トークンがコンテキストからデタッチされた時に発行されるイベント。 コンテキストとの紐づきが解除されたことを示します。 ロック状態かどうかで、即時発行か execDetachContext による発行かが変わります。

パラメータ

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
ContextUserAssigned
javascript
event ContextUserAssigned(bytes32 indexed ctxHash, uint256 indexed tokenId, address indexed user);

コンテキストユーザーが設定または更新された時に発行されるイベント。 指定コンテキストにおける利用者が更新されたことを示します。 利用者の変更をインデクサなどが追跡できます。

パラメータ

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • user
    • 新しいユーザーのアドレス。
ContextLockUpdated
javascript
event ContextLockUpdated(bytes32 indexed ctxHash, uint256 indexed tokenId, bool locked);

コンテキスト内のトークンがロックまたはアンロックされた時に発行されるイベント。 コントローラがロック状態を変更したことを示します。 デタッチ要求済みの場合はロック操作自体ができません。

パラメータ

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • locked
    • 変更後のロック状態。
OwnershipDelegationStarted
javascript
event OwnershipDelegationStarted(uint256 indexed tokenId, address indexed delegatee, uint64 until);

所有権委任が開始された時に発行されるイベント。 Owner側が委任の開始を行い、delegatee と until が設定されたことを示します。 これは「承認前の委任(pending)」を開始した合図としても扱えます。

パラメータ

  • tokenId
    • 対象NFTのトークンID。
  • delegatee
    • 委任先アドレス。
  • until
    • 委任期限のタイムスタンプ。
OwnershipDelegationAccepted
javascript
event OwnershipDelegationAccepted(uint256 indexed tokenId, address indexed delegatee, uint64 until);

所有権委任が受諾された時に発行されるイベント。 delegatee 側が委任を受け入れ、委任期間中は delegatee がOwnership Managerとして扱われる状態になったことを示します。

パラメータ

  • tokenId
    • 対象NFTのトークンID。
  • delegatee
    • 委任を受諾したアドレス。
  • until
    • 委任期限のタイムスタンプ。
OwnershipDelegationStopped
javascript
event OwnershipDelegationStopped(uint256 indexed tokenId, address indexed delegatee);

所有権委任が停止された時に発行されるイベント。 受諾済みの委任が停止され、Ownership ManagerがOwnerに戻ったことを示します。

パラメータ

  • tokenId
    • 対象NFTのトークンID。
  • delegatee
    • 停止対象の委任先アドレス。

関数

maxDetachingDuration
javascript
function maxDetachingDuration() external view returns (uint64);

デタッチ待機時間の最大値を返すという関数。 createContext と updateContext で指定できる detachingDuration の上限値を返します。

戻り値

  • uint64
    • 最大待機時間(秒)。
getContext
javascript
function getContext(bytes32 ctxHash) external view returns (address controller, uint64 detachingDuration);

コンテキストのコントローラとデタッチ待機時間を取得するという関数。 コンテキストが存在しない場合は revert します。 detachingDuration は、ロック中トークンをデタッチする際に待つ必要がある秒数です。

引数

  • ctxHash
    • コンテキストを識別するハッシュ。

戻り値

  • controller
    • コントローラのアドレス。
  • detachingDuration
    • デタッチ待機時間(秒)。
createContext
javascript
function createContext(address controller, uint64 detachingDuration, bytes calldata ctxMsg)
  external
  returns (bytes32 ctxHash);

新しいコンテキストを作成するという関数。 コンテキストが既に存在する場合は revert します。 controller がゼロアドレスの場合は revert します。 detachingDuration が maxDetachingDuration を超える場合は revert します。 成功時は ContextUpdated を emit します。 ctxMsg はコンテキスト生成の元データで、戻り値 ctxHash はそのハッシュです。

引数

  • controller
    • 作成するコンテキストのコントローラ。
  • detachingDuration
    • デタッチ待機時間(秒)。
  • ctxMsg
    • コンテキストのハッシュ化に使うメッセージ。

戻り値

  • ctxHash
    • 作成されたコンテキストのハッシュ。
updateContext
javascript
function updateContext(bytes32 ctxHash, address newController, uint64 newDetachingDuration) external;

既存コンテキストのコントローラや待機時間を更新するという関数。 呼び出し者が現在のコントローラでない場合は revert します。 コンテキストが存在しない場合は revert します。 newController がゼロアドレスの場合は revert します。 newDetachingDuration が maxDetachingDuration を超える場合は revert します。 成功時は ContextUpdated を emit します。

引数

  • ctxHash
    • 更新対象コンテキストのハッシュ。
  • newController
    • 新しいコントローラのアドレス。
  • newDetachingDuration
    • 新しいデタッチ待機時間(秒)。
isAttachedWithContext
javascript
function isAttachedWithContext(bytes32 ctxHash, uint256 tokenId) external view returns (bool);

トークンがコンテキストにアタッチされているかを返すという関数。 指定 tokenId が ctxHash のコンテキストに紐づいている場合は true を返します。

引数

  • ctxHash
    • コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。

戻り値

  • bool
    • アタッチされている場合 true。
attachContext
javascript
function attachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;

トークンをコンテキストへアタッチするという関数。 呼び出し者がOwnership Manager、Ownership Managerの承認済みオペレータ、または(委任されていない場合に限り)NFTの approved アドレスでない場合は revert します。 コンテキストが存在しない場合は revert します。 既にそのコンテキストにアタッチ済みなら revert します。 成功時は ContextAttached を emit します。 条件を満たした後、controller がコントラクトなら onAttached を必ず呼びます。 onAttached の呼び出しが失敗した場合は revert します。 data は内容を変えずに onAttached の data 引数へ渡します。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • data
    • コールバックにそのまま渡す任意データ。
requestDetachContext
javascript
function requestDetachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;

トークンのデタッチを要求するという関数。 過去に同一コンテキストについてデタッチ要求が出ている場合は revert します。 呼び出し者がコンテキストコントローラ、Ownership Manager、Ownership Managerの承認済みオペレータ、または(委任されていない場合に限り)NFTのapprovedされたアドレスでない場合は revert します。 ここからの挙動は「ロック状態」で分岐します。

状態 イベント コールバック execDetachContext
呼び出し者がコントローラ、またはロックされていない ContextDetached を emit controller がコントラクトなら onExecDetachContext を呼ぶ(結果は無視) 呼んではいけません
ロックされている ContextDetachmentRequested を emit controller がコントラクトなら onDetachRequested を呼ぶ(結果は無視) 待機後に必要です

ロックされていない場合は、デタッチがこの関数内で完了します。ロックされている場合は「要求」だけが記録され、detachingDuration 経過後に execDetachContext が必要です。 data は内容を変えずに該当するコールバックへ渡します。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • data
    • コールバックにそのまま渡す任意データ。
execDetachContext
javascript
function execDetachContext(bytes32 ctxHash, uint256 tokenId, bytes calldata data) external;

デタッチ要求済みのトークンを実際にデタッチするという関数。 呼び出し者が Ownership Manager、Ownership Managerの承認済みオペレータ、または(委任されていない場合に限り)NFTの approved されたアドレスでない場合は revert します。 デタッチ要求が存在しない場合は revert します。 さらに getContext(ctxHash) の detachingDuration を使い、要求から指定秒数が経過していない場合は revert します。 成功時は ContextDetached を emit します。 条件を満たした後、controller がコントラクトなら onExecDetachContext を必ず呼びます。 呼び出し結果は無視します。 data は内容を変えずに onExecDetachContext に渡します。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • data
    • コールバックにそのまま渡す任意データ。
getContextUser
javascript
function getContextUser(bytes32 ctxHash, uint256 tokenId) external view returns (address user);

トークンのコンテキストユーザーを取得するという関数。 指定コンテキストにおける tokenId のユーザーアドレスを返します。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。

戻り値

  • user
    • ユーザーのアドレス。
setContextUser
javascript
function setContextUser(bytes32 ctxHash, uint256 tokenId, address user) external;

コンテキストユーザーを更新するという関数。 呼び出し者がコンテキストコントローラでない場合は revert します。 コンテキストが存在しない場合は revert します。 トークンがそのコンテキストにアタッチされていない場合は revert します。 成功時は ContextUserAssigned を emit します。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • user
    • 新しいユーザーのアドレス。
isTokenContextLocked
javascript
function isTokenContextLocked(bytes32 ctxHash, uint256 tokenId) external view returns (bool);

トークンがコンテキスト内でロックされているかを返すという関数。 ロック中の場合、デタッチは即時実行できず、requestDetachContext → 待機 → execDetachContext の流れになります。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。

戻り値

  • bool
    • ロックされている場合 true。
setContextLock
javascript
function setContextLock(bytes32 ctxHash, uint256 tokenId, bool lock) external;

コンテキスト内のトークンをロックまたはアンロックするという関数。 コンテキストが存在しない場合は revert します。 トークンがそのコンテキストにアタッチされていない場合は revert します。 過去にデタッチ要求が出ている場合は revert します。 呼び出し者がコンテキストコントローラでない場合は revert します。 成功時は ContextLockUpdated を emit します。 シナリオとして、デタッチ要求がない場合はロック操作が成功し、デタッチ要求がある場合は必ず revert します。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • lock
    • 新しいロック状態。
getOwnershipManager
javascript
function getOwnershipManager(uint256 tokenId) external view returns(address manager);

現在のOwnership Managerを取得するという関数。 委任が有効で受諾済みなら delegatee を返し、それ以外はOwnerを返す想定の参照口です。

引数

  • tokenId
    • 対象NFTのトークンID。

戻り値

  • manager
    • Ownership Managerのアドレス。
getOwnershipDelegatee
javascript
function getOwnershipDelegatee(uint256 tokenId) external view returns (address delegatee, uint64 until);

現在有効な所有権委任の delegatee と期限を取得するという関数。 有効な委任が存在しない、または期限切れの場合は revert します。

引数

  • tokenId
    • 対象NFTのトークンID。

戻り値

  • delegatee
    • 委任先のアドレス。
  • until
    • 委任期限のタイムスタンプ。
pendingOwnershipDelegatee
javascript
function pendingOwnershipDelegatee(uint256 tokenId) external view returns (address delegatee, uint64 until);

未受諾の所有権委任(pending)の delegatee と期限を取得するという関数。 pending が存在しない、または期限切れの場合は revert します。

引数

  • tokenId
    • 対象NFTのトークンID。

戻り値

  • delegatee
    • pending の委任先アドレス。
  • until
    • 委任期限のタイムスタンプ。
startDelegateOwnership
javascript
function startDelegateOwnership(uint256 tokenId, address delegatee, uint64 until) external;

所有権委任を開始するという関数。 委任が存在しない場合にしか成功しません。 呼び出し者が Owner、Ownerの承認済みオペレータ、またはNFTの approved されたアドレスでない場合は revert します。 until が未来でない場合は revert します。 delegatee がOwnerまたはゼロアドレスの場合は revert します。 token が存在しない場合は revert します。 成功時は OwnershipDelegationStarted を emit します。 条件を満たした後、pending が既にある場合は置き換えます。

引数

  • tokenId
    • 対象NFTのトークンID。
  • delegatee
    • 委任先アドレス。
  • until
    • 委任期限のタイムスタンプ。
acceptOwnershipDelegation
javascript
function acceptOwnershipDelegation(uint256 tokenId) external;

所有権委任を受諾するという関数。 委任が存在しない場合は revert します。 呼び出し者が delegatee または delegatee の承認済みオペレータでない場合は revert します。 until が現在時刻以降でない場合は revert します。 成功時は OwnershipDelegationAccepted を発行します。 条件を満たした後、期限まで delegatee をOwnership Managerとして記録します。

引数

  • tokenId
    • 対象NFTのトークンID。
stopOwnershipDelegation
javascript
function stopOwnershipDelegation(uint256 tokenId) external;

受諾済みの所有権委任を停止するという関数。 委任が受諾済みでない場合は revert します。 until が未来でない場合は revert します。 呼び出し者が delegatee または delegatee の承認済みオペレータでない場合は revert します。 成功時は OwnershipDelegationStopped イベントを発行します。 条件を満たした後、OwnerをOwnership Managerとして記録します。

引数

  • tokenId
    • 対象NFTのトークンID。

Enumerable extension

IERC7695Enumerable.sol
/// Note: the ERC-165 identifier for this interface is 0xcebf44b7.
interface IERC7695Enumerable /* is IERC165 */ {
  /// @dev Returns a created context in this contract at `index`.
  /// An index must be a value between 0 and {getContextCount}, non-inclusive.
  /// Note: When using {getContext} and {getContextCount}, make sure you perform all queries on the same block.
  function getContext(uint256 index) external view returns(bytes32 ctxHash);

  /// @dev Returns the number of contexts created in the contract.
  function getContextCount() external view returns(uint256);

  /// @dev Returns a context attached to a token at `index`.
  /// An index must be a value between 0 and {getAttachedContextCount}, non-inclusive.
  /// Note: When using {getAttachedContext} and {getAttachedContextCount}, make sure you perform all queries on the same block.
  function getAttachedContext(uint256 tokenId, uint256 index) external view returns(bytes32 ctxHash);

  /// @dev Returns the number of contexts attached to the token.
  function getAttachedContextCount(uint256 tokenId) external view returns(uint256);
}
getContext
javascript
function getContext(uint256 index) external view returns(bytes32 ctxHash);

コントラクト内で作成されたコンテキストをインデックスで取得するという関数。 index は0以上 getContextCount() 未満である必要があります。 同一ブロックで getContextCount と合わせて参照する前提です。

引数

  • index
    • 取得したいコンテキストのインデックス。

戻り値

  • ctxHash
    • コンテキストのハッシュ。
getContextCount
javascript
function getContextCount() external view returns(uint256);

コントラクト内で作成されたコンテキスト数を返すという関数。 Enumerable 拡張で全コンテキストを列挙するための総数を返します。

戻り値

  • uint256
    • コンテキスト数。
getAttachedContext
javascript
function getAttachedContext(uint256 tokenId, uint256 index) external view returns(bytes32 ctxHash);

トークンにアタッチされたコンテキストをインデックスで取得するという関数。 index は0以上 getAttachedContextCount(tokenId) 未満である必要があります。 同一ブロックで getAttachedContextCount と合わせて参照する前提です。

引数

  • tokenId
    • 対象NFTのトークンID。
  • index
    • アタッチ済みコンテキストのインデックス。

戻り値

  • ctxHash
    • コンテキストのハッシュ。
getAttachedContextCount
javascript
function getAttachedContextCount(uint256 tokenId) external view returns(uint256);

トークンにアタッチされたコンテキスト数を返すという関数。 特定NFTに紐づくコンテキストを列挙するための総数を返します。

引数

  • tokenId
    • 対象NFTのトークンID。

戻り値

  • uint256
    • アタッチされているコンテキスト数。

Controller Interface

IERC7695ContextCallback.sol
/// Note: the ERC-165 identifier for this interface is 0xad0491f1.
interface IERC7695ContextCallback /* is IERC165 */  {
  /// @dev This method is called once the token is attached by any mechanism.
  /// This function MAY throw to revert and reject the attachment.
  /// @param ctxHash  The hash of context invoked this call.
  /// @param tokenId  NFT identifier which is being attached.
  /// @param operator The address which called {attachContext} function.
  /// @param data     Additional data with no specified format.
  function onAttached(bytes32 ctxHash, uint256 tokenId, address operator, bytes calldata data) external;

  /// @dev This method is called once the token detachment is requested by any mechanism.
  /// @param ctxHash  The hash of context invoked this call.
  /// @param tokenId  NFT identifier which is being requested for detachment.
  /// @param operator The address which called {requestDetachContext} function.
  /// @param data     Additional data with no specified format.
  function onDetachRequested(bytes32 ctxHash, uint256 tokenId, address operator, bytes calldata data) external;

  /// @dev This method is called once a token context is detached by any mechanism.
  /// @param ctxHash  The hash of context invoked this call.
  /// @param tokenId  NFT identifier which is being detached.
  /// @param user     The address of the context user which is being detached.
  /// @param operator The address which called {execDetachContext} function.
  /// @param data     Additional data with no specified format.
  function onExecDetachContext(bytes32 ctxHash, uint256 tokenId, address user, address operator, bytes calldata data) external;
}
onAttached
javascript
function onAttached(bytes32 ctxHash, uint256 tokenId, address operator, bytes calldata data) external;

トークンがアタッチされた後に呼び出されるという関数。 attachContext 実行時に controller がコントラクトなら呼ばれます。 ここは revert 可能で、revert するとアタッチ自体が失敗します。 data は呼び出し元の data がそのまま渡されます。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • operator
    • attachContext を呼んだアドレス。
  • data
    • 任意データ。
onDetachRequested
javascript
function onDetachRequested(bytes32 ctxHash, uint256 tokenId, address operator, bytes calldata data) external;

ロック中トークンのデタッチが要求された時に呼び出されるという関数。 requestDetachContext でロック中のデタッチ要求が成立した場合に、controller がコントラクトなら呼ばれます。 呼び出し結果は無視され、成功失敗に関わらず処理は継続します。 data は呼び出し元の data がそのまま渡されます。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • operator
    • requestDetachContext を呼んだアドレス。
  • data
    • 任意データ。
onExecDetachContext
javascript
function onExecDetachContext(bytes32 ctxHash, uint256 tokenId, address user, address operator, bytes calldata data) external;

トークンがデタッチされた時に呼び出されるという関数。 execDetachContext によるデタッチ完了時、または requestDetachContext による即時デタッチ時に、controller がコントラクトなら呼ばれます。 呼び出し結果は無視され、成功失敗に関わらず処理は継続します。 data は呼び出し元の data がそのまま渡されます。

引数

  • ctxHash
    • 対象コンテキストのハッシュ。
  • tokenId
    • 対象NFTのトークンID。
  • user
    • デタッチ対象のコンテキストユーザー。
  • operator
    • execDetachContext を呼んだアドレス。
  • data
    • 任意データ。

補足

複数ユースケースに対応する複数コンテキスト

ERC7695は、NFTを「1つの用途でしか使えない資産」として扱う設計を見直しています。 その中心にあるのが「トークンコンテキスト」という考え方です。

トークンコンテキストは、「このNFTを、どの用途で、誰が使うのか」を切り分けるための枠組みです。 これにより、同じNFTであっても、レンタル用途、委任用途、ステーキング用途などを同時に、かつ独立して扱えるようになります。

各コンテキストにはコントローラが存在します。 このコントローラは、レンタルや委任などのdAppsを成立させるための管理主体として振る舞います。 重要なのは、利用権の付与や管理を行っても、NFTのOwner自体は変更されない点です。

さらに、コンテキスト単位でロック機能を提供することで、信頼不要な形でdAppsを構築できます。 特にステーキングのように「一定期間NFTを動かせないこと」が前提となるユースケースでは、このロック機能が不可欠になります。

オーナー自身によるアンロック機構の提供

ERC7695では、NFTがロックされている場合でも、オーナー側が主体的にアンロックのプロセスを進められる設計になっています。 従来の設計では、ロックを解除する権限がコントローラ側に強く依存していると、以下のような問題が起き得ます。

  • コントローラの秘密鍵が失われる
  • コントローラのコントラクトがバグや停止状態になる
  • 運営主体が意図的に操作を行わなくなる

このような状況では、NFTのOwnerが何もできず、資産が事実上凍結されてしまいます。

ERC7695は、Owner(またはOwnership Manager)が自らデタッチ要求を出し、所定の待機時間を経てデタッチを完了できる仕組みを用意しています。 これにより、「コントローラが不在・無反応でもNFTを取り戻せる」状態を保証し、資産としての安全性を高めています。

アタッチ・デタッチ時のコールバック設計

ERC7695では、コンテキストのアタッチ・デタッチに応じて、コントローラにコールバックが通知されます。 ただし、すべてのコールバックが同じ扱いではありません。

デタッチ関連のコールバックである onDetachRequested と onExecDetachContext については、呼び出し結果が意図的に無視されます。 これは、コントローラが「デタッチを止められる存在」にならないようにするためです。

つまり、コントローラがどのような実装であっても、Owner側の意思でデタッチが完了することを妨げられません。 これにより、デタッチ操作は常にOwner側主導で完結し、コントローラに依存しない設計になります。

一方で、アタッチ時の onAttached コールバックは例外です。 こちらは、コールバックが失敗した場合に処理全体を revert します。

これは、「このコンテキストには、このNFTを受け入れられない」という判断をコントローラ側が明確に示せるようにするためです。 結果として、デタッチは止められないが、アタッチは拒否できる、という非対称な設計になっています。

所有権委任

ERC7695は、「Ownerであること」と「所有権の操作権限を持つこと」を明確に分離します。 これがOwnership Delegationの考え方です。

一般的なERC721では、approve や setApprovalForAll によって他人に操作権限を与えますが、所有者は常にOwnerのままです。 そのため、Ownerが権限を保持したまま、意図しない操作を行う余地が残ります。

Ownership Delegationでは、単なる許可付与ではなく、「所有権の権利そのもの」を一定期間、別アカウントに移します。 委任期間中は、Owner自身であっても所有権操作を行えません。期限が来ると、自動的にOwnerへ戻ります。

この設計により、以下のような問題を防げます。

  • 担保に出している最中に、Ownerが別アカウントへ勝手に移転する
  • 委任中にもかかわらず、Ownerが権限を使って状態を変更する

また、委任は2段階(オファー → 承認)で成立します。 これにより、誤ったアドレスを指定してしまう事故を防ぎます。 さらに、期限前に委任を取り消したい場合は、delegatee 側から stopOwnershipDelegation を呼び出せます。

転送メカニズムとの統合

ERC7695は、NFTの転送時の挙動も拡張しています。 これはユーザー体験と安全性の両立を目的とした設計です。

NFTが転送されるとき、内部的に以下の処理が行われます。

処理内容 挙動の意図
所有権委任の解除 新しい所有者に過去の委任状態を引き継がせないため
全コンテキストのデタッチ 前所有者の利用文脈を完全にリセットするため

これらの処理では、対応するイベント(OwnershipDelegationStopped や ContextDetached)は意図的に発行されません。 これは、ガスコスト削減と処理の単純化を優先した結果です。

その代わり、状態としては確実にリセットされるため、トラストレス性は維持されます。

互換性

ERC7695はERC721と後方互換性があります。

既存のERC721NFTとしての基本的な振る舞い(所有、転送、承認など)はそのまま維持されます。 ERC7695で追加される機能は、あくまで拡張であり、既存のERC721対応ツールやマーケットプレイスを壊しません。

そのため、以下のように段階的な導入が可能です。

  • ERC721として普通に使うこともできる
  • 必要な場面だけERC7695の拡張機能を利用できる

これにより、既存エコシステムとの共存を前提とした、現実的な標準設計になっています。

セキュリティ

デタッチ待機時間

ERC7695では、1つのNFTを複数のコンテキストで利用できる設計になっているため、「いつデタッチできるのか」という時間の扱いが非常に重要になります。

まず、コントラクトをデプロイする側は、maxDetachingDuration によって「デタッチ待機時間の最大値」を適切に設定する必要があります。 これは、このNFT全体に対する安全弁のような役割を持ちます。

その上で、各コンテキストの管理者(コントローラ)は、想定しているユースケースを踏まえ、最大値を超えない範囲で適切な detachingDuration を設定します。

この仕組みが必要な理由は、Ownerによる悪用を防ぐためです。 もしデタッチ待機時間が極端に短い、または制限がなければ、以下のような行動が可能になります。

  • NFTをマーケットやdAppに大量に出品する
  • 短時間で別のOwnerに移転する
  • 利用権や評価システムを混乱させる

上限と待機時間を設けることで、「すぐに逃げられるNFT」になることを防ぎ、エコシステム全体の健全性を保ちます。

トークンの重複利用

ERC7695では、1つのNFTに複数のコンテキストを作成できますが、その自由度は同時にリスクも生みます。

コンテキストコントローラは、新しいコンテキストを作成・運用する時に、そのNFTにすでに存在する他のコンテキストを把握し、重複利用が起きないように設計する必要があります。

例えば、以下のようなケースです。

  • あるゲームで、NFTが「レンタル用コンテキスト」によってロックされている
  • 同じゲーム内で、別の「委任用コンテキスト」を新たに作成する
  • その結果、同じNFTがゲーム内で二重に利用されてしまう

これは、コンテキストとしては別でも、実際のアプリケーション上では不正利用に近い状態になります。

こうした問題を避けるためには、以下のような設計が考えられます。

  • レンタルと委任を1つの共通コンテキストとして扱う
  • 新しいコンテキストに制限を設け、既存コンテキストと同時利用できないようにする

ERC7695は自由度を提供しますが、「どう組み合わせるか」の責任はコントローラ側にあります。

所有権委任におけるバッファ時間

所有権委任を前提としたシステムを構築する場合、委任期間には必ず十分なバッファ時間を設ける必要があります。 最低でも maxDetachingDuration 秒以上の余裕を持つことが推奨されます。

これは、コンテキストがNFTをロックできる設計であるためです。

具体例で考えます。

  • 最大デタッチ待機時間が7日
  • 委任期間が3日しかない
  • 担保(モーゲージ)用のコントラクトが存在する

この状態で、Ownerが資金を引き出した直後に、関連するコンテキストでNFTをロックしてしまうと、委任期間内にNFTを回収・移転できなくなります。

結果として、システム側はNFTを確保できず、悪用される可能性が生まれます。 そのため、委任期間は「最悪のロック期間を考慮した長さ」に設定し、時間設計そのものをセキュリティ要件として扱う必要があります。

コールバック呼び出し元の検証

IERC7695ContextCallback を実装する場合、コールバック関数の msg.sender を必ず検証することが重要です。

理由は単純で、悪意のある第三者が「正規のコールバックであるかのように振る舞う」可能性があるためです。

実装時には、以下のような前提を明確にする必要があります。

  • コールバックを呼ぶのはNFTコントラクトか
  • あるいは、事前に許可された特定のコントローラコントラクトか

これをチェックしないと、外部の不正コントラクトがコールバック関数を直接呼び出し、本来想定していない処理を実行させる恐れがあります。

推奨される利用パターン

レンタル(Rental)

レンタルは ERC7695 における代表的なユースケースです。 想定される状況は以下の通りです。

  • A(Owner)がNFTを所有している
  • B(User)が、そのNFTを使って特定のゲームをプレイしたい

この場合、NFTはレンタル用のコンテキストにアタッチされ、UserとしてBが設定されます。 Ownerは変わらないため、保有者向けの特典や権利はAに残ります。

ロック機能を使えば、レンタル期間中にNFTが勝手に移転されることも防げます。

モーゲージ

担保型システムでは、「NFTを担保に出しながら、同時に活用したい」というニーズが生まれます。

ERC7695では、NFTを担保に使いつつ、レンタルなど別のコンテキストで活用する設計も可能です。 これにより、以下のような柔軟な金融モデルが成立します。

  • A(Owner)はNFTを担保に資金を借りる
  • B(Lender)は利息を得る
  • NFTはレンタルなどで追加収益を生む可能性がある

このような設計は、モーゲージシステム自体の魅力を高め、より多様な参加者を呼び込むことにつながります。

トークンOwnerに関するリスク

フィッシング攻撃

ERC7695では、Ownerが所有権を別アカウントに委任できます。 これは強力な機能である一方、フィッシングのリスクも伴います。

悪意のある第三者が、Ownerを騙して startDelegateOwnership を実行させ、自分をdelegateeに設定させる可能性があります。 このリスクは、approve や setApprovalForAll と本質的に同じです。

そのため、Ownerは「委任=強い権限移譲」であることを十分理解した上で操作する必要があります。

所有権喪失のリスク

担保や金融系コントラクトと連携する場合、Ownerは一時的に所有権の権利をコントラクトへ委任することになります。

このとき、以下の点に注意が必要です。

注意点 理由
委任期間が長すぎないか 期限まで権利が戻らないため
途中で委任を解除できる仕組みがあるか 想定外の拘束を防ぐため
コントラクト管理者が安全か 管理者やオペレータが侵害されると、所有権が操作される可能性があるため

特に、解除手段がない長期委任は、実質的な所有権喪失につながります。

ERC7695は柔軟な仕組みを提供しますが、安全に使うためには、Owner・開発者双方がリスクを正しく理解した上で設計・運用することが不可欠です。

引用

Duc Tho Tran (@ducthotran2010), "ERC-7695: Ownership Delegation and Context for ERC-721 [DRAFT]," Ethereum Improvement Proposals, no. 7695, April 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7695.

最後に

今回は「所有者を変更せずにNFTの利用権だけを別のユーザーに委ねられる仕組みを提案しているERC7695」についてまとめてきました! いかがだったでしょうか?

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

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