はじめに

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

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

今回は「ERC-1155をウォレットに残したままロックするERC-7721」についてまとめていきます。

ERC-7721は、ERC-1155の保有者がトークンIDと数量を指定して譲渡を止め、解除できるアカウントを分けるための提案です。 NFTを貸付やレンタルで使う時に、保有先をエスクロー用コントラクトへ移さず、ロック量だけを記録します。

ERC-7721はDraftの提案です。

概要

ERC-7721は、ERC-1155へロックと解除の操作を追加する拡張です。 ロックされた数量は保有者のERC-1155残高に残りますが、通常の送付で動かせる数量から除かれます。

保有者のウォレットにNFTを残し、ロック記録によって譲渡を停止する関係

NFTそのものを相手のコントラクトへ預けるのではありません。 提案が記録するのは、どのトークンIDを、どの保有アカウントで、誰が解除できる状態にし、何個をロックしたかです。

ERC-7721は、ERC-165とERC-1155を前提にしています。 ERC-1155は一つのコントラクトで複数種類のトークンIDと数量を扱えるため、ロックもトークンIDと数量を指定して行います。

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

動機

NFTを貸付の担保にする時、貸付コントラクトへ送るエスクロー方式を選ぶことがあります。 ただし、ゲームや会員資格の仕組みが保有者のアドレスを見て利用可否を決める場合、NFTをエスクローへ送ると利用まで止まります。

ERC-7721は、保有アドレスを変えずに譲渡だけを止める案です。 提案では、担保にしたNFTをゲームで使い続けるケース、借り手がNFTを使いながら再譲渡を止めるレンタル、支払い完了まで再販売を止める分割払いを例に挙げています。

ロックの開始時刻、返済額、満了時刻、返還条件はERC-7721には入りません。 貸付やレンタルのコントラクトが条件を確認し、条件を満たした時にロック解除を呼び出す構成です。

仕様

ロックの記録は、トークンID、保有アカウント、解除者、数量に関わります。 提案には、保有者が解除者を指定する形式と、トークンIDごとの承認を受けたアカウントが自分を解除者にする形式があります。

保有者が解除者を指定してロックし、解除者だけが解除する流れ

保有者が指定した解除者はEOAでもコントラクトでも構いません。 一方、承認を受けたアカウントがロックする形式では、解除者は呼び出したアカウント自身になります。

ロックと解除

lock(uint256,address,uint256)

js
function lock(uint256 tokenId, address account, uint256 amount) external;

この3引数のlockは、トークンIDと数量をロックし、呼び出し元自身を解除者として記録する呼び出しです。

  • 引数
引数 詳細
tokenId ロックするERC-1155のトークンIDです。
account ロック対象を保有するアカウントです。
amount ロックする数量です。
  • 実行条件

    • 呼び出し元は、保有者またはトークンID単位の承認者です。
    • accountのtokenId残高は、amount以上です。
    • 条件を満たさない時、この形式のlockはrevertします。
  1. 保有権限と残高を確認する

    accountがamountを保有し、呼び出し元がロックを設定できるかを確認します。

  2. 呼び出し元を解除者として記録する

    対象のtokenId、account、呼び出し元、amountの組み合わせでロックが作られます。

  3. 通常の送付に使える数量を減らす

    ロックしたamountは送付可能な数量から除かれます。 貸付コントラクトがこの形式を使う場合、同じコントラクトが返済後にunlockを呼べます。

lock(uint256,address,address,uint256)

js
function lock(uint256 tokenId, address account, address _locker, uint256 amount) external;

この4引数のlockは、保有者が解除者を別のアカウントへ指定してロックする呼び出しです。

  • 引数
引数 詳細
tokenId ロックするERC-1155のトークンIDです。
account ロック対象を保有するアカウントです。
_locker ロック後に解除できるアカウントです。
amount ロックする数量です。
  • 実行条件

    • 呼び出し元は保有者です。
    • accountのtokenId残高は、amount以上です。
    • 条件を満たさない時、この形式のlockはrevertします。
  1. 保有者と残高を確認する

    保有者だけが、別の解除者を指定してロックできます。 トークンID単位の承認を受けたアカウントは、この形式で他人を解除者に指定できません。

  2. 指定した解除者でロックを記録する

    対象のtokenId、account、_locker、amountの組み合わせでロックが作られます。

保有者が日常利用のウォレットでNFTを持ち続け、コールドウォレットを_lockerに指定できます。 日常利用のウォレットだけを取得した攻撃者は、ロック済み数量を送れません。

unlock

js
function unlock(uint256 tokenId, address account, uint256 amount) external;

unlockは、ロック済みの数量を減らし、再び送付できる数量を増やす呼び出しです。

  • 引数
引数 詳細
tokenId 解除するERC-1155のトークンIDです。
account ロック対象を保有するアカウントです。
amount 解除する数量です。
  • 実行条件

    • 呼び出し元は、対象のtokenIdとaccountのロックで解除者として記録されたアカウントです。
    • 指定するamountは、その解除者がロックした数量以下です。
    • 条件を満たさない時、unlockはrevertします。
  1. 解除者のロック数量を確認する

    tokenId、account、呼び出し元に対応するロック数量を確認します。

  2. 指定数量をロックから外す

    ロックした数量の一部だけを解除する場合も、同じtokenIdとaccountを指定します。

  3. 送付可能な数量を増やす

    指定した解除者によるロック数量が減り、同じ数量だけ通常の送付に使える残高が増えます。 返済完了やレンタル期限切れを確認するコントラクトを解除者にすれば、その条件を満たした時だけ呼び出せます。

setApprovalForId

js
function setApprovalForId(
    uint256 tokenId,
    address operator,
    uint256 amount
) external;

setApprovalForIdは、あるtokenIdについて、operatorが扱える数量を設定します。

  • 引数
引数 詳細
tokenId 承認するERC-1155のトークンIDです。
operator トークンID単位の承認を受けるアカウントです。
amount 承認する数量です。
  • 実行条件

    • 呼び出し元はトークン所有者です。
    • 提案はこの呼び出し固有のrevert条件を列挙していません。
  1. トークンIDと数量を限定して承認する

    ERC-1155のsetApprovalForAllが保有者の全トークンを対象にするのに対し、こちらは一つのトークンIDと数量だけに絞ります。

  2. 承認量を記録する

    operatorがtokenIdについて扱えるamountが記録されます。

貸付コントラクトへロックを許可する時に、別のトークンIDまで操作できる権限を渡さずに済みます。 承認後、operatorは3引数のlockを呼び、自分を解除者としてロックできます。

getApprovalForId

js
getApprovalForId(uint256 tokenId, address account, address operator)

getApprovalForIdは、operatorがaccount上のtokenIdについて扱える承認量を参照する呼び出しです。 掲載されたインターフェース断片には完全なSolidityシグネチャと戻り値型がないため、提案の概要にある呼び出し形だけを示します。

  • 引数
引数 詳細
tokenId 承認量を確認するERC-1155のトークンIDです。
account 承認を設定したトークン所有者のアカウントです。
operator 承認量を確認するアカウントです。
  • 戻り値
戻り値 詳細
承認量 operatorがaccount上のtokenIdについて扱える数量です。戻り値のSolidity型は掲載インターフェースにありません。
  • 実行条件

    • 提案は呼出主体の制限を定めていません。
    • 提案はこの参照呼び出し固有のrevert条件を列挙していません。

アプリケーションは、トークン所有者がsetApprovalForIdで設定した承認量を確認します。 この値を使うと、あるoperatorが3引数のlockを呼べる条件を確認できます。

getLocked

js
function getLocked(
    uint256 tokenId,
    address account,
    address operator
) external view returns (uint256);

getLockedは、指定したoperatorがaccount上でロックしているtokenIdの数量を返す参照用の呼び出しです。

  • 引数
引数 詳細
tokenId 参照するERC-1155のトークンIDです。
account ロック対象を保有するアカウントです。
operator ロック数量を参照する解除者です。
  • 戻り値
戻り値 詳細
uint256 その解除者がロックしている数量です。ロックがなければ0を返します。
  • 実行条件

    • 提案は呼出主体の制限を定めていません。
    • 指定した組み合わせにロックがない時、0を返します。
    • 提案はこの参照呼び出し固有のrevert条件を列挙していません。

アプリケーションは、確認したいtokenId、保有者、解除者を渡して、その解除者がロックした数量を読み取ります。 送付を実行するコントラクトは、実装が定めるロック量の集計を使って送付可能な上限を検査します。

ERC-1155のsafeTransferFromは、ロック済み数量を送ろうとした時にrevertしなければなりません。 提案の概要は、譲渡できる上限を残高からロック量を引いた値として示しています。

残高5のうち3をロックすると、譲渡できるのは2だけになる例

図の例は、1人の解除者が残高5のうち3をロックした場合です。 数量2の送付は通り、数量3の送付は拒否されます。 複数の解除者によるロックをどう集計して送付前に検査するかは、このインターフェース断片だけでは判断できません。 バッチ送付も含め、採用する実装の残高検査をテストで確認します。

transferAndLock

js
function transferAndLock(
    address from,
    address to,
    uint256 tokenId,
    uint256 amount,
    bool setApprove
) external;

transferAndLockは、トークンを受取先へ送り、受取先上の数量をロックする操作を一つにまとめます。

  • 引数
引数 詳細
from 送付元のアカウントです。
to トークンを受け取り、ロック後に保有するアカウントです。
tokenId 送付してロックするERC-1155のトークンIDです。
amount 送付してロックする数量です。
setApprove trueなら、ロック者になる呼び出し元へトークンID単位の承認を設定します。
  • 実行条件

    • 保有者または承認者が呼び出せます。
    • 呼び出し元がロック者になります。
    • setApproveがtrueなら、同じ呼び出し元へトークンID単位の承認を設定します。
    • 提案はこの呼び出し固有のrevert条件を列挙していません。
  1. 受取先へ送付し、受取先上でロックする

    この単一操作で、受取先への送付と受取先上のロックが成立します。

  2. 必要な時だけ承認を付ける

    setApproveがtrueなら、呼び出し元へトークンID単位の承認も設定されます。

提案は、実装内部でこれらを処理する順序までは定めていません。 未払い時に回収する分割払いでは、送付後の回収条件を別コントラクトへ置きます。

受取先へ送付してすぐロックし、必要な時だけ承認を付ける流れ

この操作は、受取人がNFTを利用できても再譲渡できない状態を作る時に使えます。

トークンが移転すると、以前のロックやトークンID単位の承認に関する権利は消去されるとAbstractで説明されています。 transferAndLockは、移転と新しいロック、必要なら新しい承認を一つの操作にするために用意されています。

イベント

Lock

js
event Lock(
    uint256 indexed tokenId,
    address account,
    address _locker,
    uint256 amount
);

Lockイベントは、トークンIDと数量がロックされた時に発行されます。

  • フィールド
フィールド 詳細
tokenId ロックしたERC-1155のトークンIDです。インデックス付きで記録されます。
account ロック対象を保有するアカウントです。
_locker ロックを解除できるアカウントです。
amount ロックした数量です。

lockまたはtransferAndLockによってロック状態が作られる時、このイベントが発行されます。 インデクサーは、このイベントから誰のどのトークンIDが、誰の解除権限で止まったかを追えます。 ただし、トークン移転時にロックと承認の権利が消去される処理は、このイベントだけでは追えません。

Unlock

js
event Unlock(
    uint256 indexed tokenId,
    address account,
    address _locker,
    uint256 amount
);

Unlockイベントは、ロック済み数量を解除した時に発行されます。

  • フィールド
フィールド 詳細
tokenId 解除したERC-1155のトークンIDです。インデックス付きで記録されます。
account 解除対象を保有するアカウントです。
_locker 解除を実行した解除者です。
amount 解除した数量です。

unlockが成功すると、このイベントが発行されます。 LockとUnlockは、明示的なロックと解除の数量変化を追うためのイベントです。 ただし、提案はトークン移転時に以前のロックと承認の権利を消去すると説明しており、その消去にUnlockイベントを発行するとは定めていません。 現在のロック状態を再構築するインデクサーは、ERC-1155の送付イベントと採用する実装の移転時処理も確認します。

補足

時間制限を標準へ固定しない

ERC-7721は時間制限を持ちません。 提案は、返済、期限切れ、レンタル終了といった条件を共通インターフェースへ固定せず、ロックを解除する条件は別のコントラクトが決める形を選んでいます。

そのため、lockerをコントラクトにする場合は、そのコントラクトがいつunlockを呼べるのかを個別に読みます。 ERC-7721だけを見ても、返済完了、期限切れ、没収の条件は分かりません。

ロックと回収は別の権限

ロックは通常の譲渡を止める仕組みです。 提案のセキュリティ上の注意には、呼び出し元がlockerであり、トークンID単位の承認も持つ場合、ロック済みトークンを移転できるとあります。

setApproveを伴うtransferAndLockを使う時は、ロックだけでなく回収に使える権限も渡る可能性があります。 貸付や分割払いの画面では、誰がどの条件でNFTを移せるのかを明示します。

互換性

ERC-7721はERC-1155に互換な拡張として提案されています。 既存のアップグレード可能なERC-1155コントラクトはロック機能を追加できますが、送付処理がロック済み数量を確実に除外しなければ提案の要件を満たしません。

対応していないERC-1155へアプリケーション側だけでロックを前提にすることはできません。 対象コントラクトがこの拡張を実装しているかを、ERC-165の対応状況と実装コードで確認します。

参考実装

提案はインターフェースと実装例へのリンクを掲載しています。 掲載された提案本文は内部ストレージやバッチ送付の実装詳細までは定めていないため、採用する実装のコードとテストを確認します。

実装では、少なくとも以下を確認します。

  • ロック量の集計 複数回のロックと部分解除の後に、送付可能量が正しく計算されるかを確認します。
  • すべての送付経路 safeTransferFromだけでなく、バッチ送付や承認済みオペレーターによる経路でもロック量を超えて動かせないことを確認します。
  • 移転後の権利 移転時に以前のロックと承認がどの順で消去され、transferAndLockで新しい状態がどの順で作られるかを確認します。

セキュリティ

解除者を失うとロックが残る

解除者に指定したEOAへアクセスできなくなった場合や、解除処理を持たないコントラクトを指定した場合、ロック済みトークンが恒久的に動かなくなる可能性があります。 保有者が普段使うウォレットと解除者を分けるなら、解除者を失った時の復旧方法まで決めます。

送付経路の検査漏れ

ロックの目的は、ロック量を通常の送付で動かせなくすることです。 一つの送付関数だけを検査しても、バッチ送付やオペレーター経由の経路が残ればロックを回避できます。

条件コントラクトの確認

ERC-7721は解除の権限を分けますが、解除する条件までは決めません。 レンタル、貸付、分割払いでは、lockerになるコントラクトが持つ期限、返済、回収の条件と管理者権限を個別に確認します。

最後に

今回は「ERC-1155をウォレットに残したままロックするERC-7721」についてまとめてきました。

ERC-7721は、ERC-1155の保有先を変えず、トークンIDと数量ごとに譲渡を止める提案です。 保有者、承認者、解除者の役割を分け、貸付やレンタルの条件は外部コントラクトへ置きます。 採用する時は、ロック量の集計、すべての送付経路、transferAndLockで渡る承認を実装ごとに確認します。

他でも色々記事を書いているのでぜひよろしければ読んでいってください!