5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[ERC5725] NFTを使用したべスティングの仕組みを理解しよう!

Posted at

はじめに

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

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

今回は、NFTを使用して段階的にERC20トークンを解放するべスティングのインターフェースを提案するERC5725についてまとめていきます!

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

ERC5725は現在(2023年9月3日)では「Last Call」段階です。

概要

この規格は、NFT(非代替性トークン)を使用して、特定のトークン(たとえばERC20トークン)を段階的に解放する方法を提供するものです。
具体的には、トークンの受取アドレスに対して、一定期間にわたってトークンを少しずつ解放するメカニズムを提供します。
これにより、トークンの受取アドレスはトークンを一気にすべて受け取るのではなく、時間とともに解放されるようになります。

この規格は、NFT関連コントラクトの標準的なインターフェースを定義しています。
NFTの保持者にとって重要な情報である、基盤となるトークン(ERC20など)のベスト(解放)スケジュールやロックされた状態などのプロパティにNFTを使用することができます。

動機

Vestingコントラクトやタイムロックコントラクトなどは、現在標準化(統一)された方法が欠けており、その結果、さまざまな実装が存在しています。
こうしたコントラクトを1つの統一されたインターフェースに基づいて標準化することで、これらのコントラクトに関連するツールがオンチェーンやオフチェーンで統一されたエコシステムを形成できるようになります。
また、非代替性アセットの形で流動性を持たせたベスティングは、従来の未来トークンのための契約(SAFT)や外部所有アカウント(EOA)ベースのベスティングと比べて、大きな改善となる可能性があります。

Simple Agreement for Future Tokens(SAFT)
将来的に発行される予定のトークンに対するコントラクトです。
SAFTは、早期にプロジェクトに資金を提供する投資家と、プロジェクトがトークンを実際に発行する段階でそのトークンを受け取る権利を持つ投資家との間で取り決めを行うための仕組みです。

具体的には、プロジェクトがまだトークンを発行していない段階で、投資家はプロジェクトに資金を提供します。
この資金はプロジェクトの開発や運営に使用され、将来的にトークンが発行されると、投資家はそのトークンを予め取得する権利を持つことになります。
この契約に基づいて、トークンが発行されると投資家は取得権を行使し、トークンを受け取ることができます。

SAFTは、プロジェクトがトークンを発行するための法的・資金的な基盤を整える際に使用されることがあります。
投資家は将来のトークン価値の上昇を期待してプロジェクトに資金を提供し、その見返りとしてトークンを受け取る仕組みとなります。
ただし、トークンが実際に発行されるタイミングや条件はSAFTの契約によって異なる場合があります。

SAFTは、トークンセールやプロジェクトの資金調達手段の一つとして広く用いられてきました。

これにより、トークンは時間をかけて解放されるだけでなく、譲渡可能性を持ち、既存の従来型NFTと同様のメタデータを添付できるようになります。

この標準化により、必要とされていたERC20トークンのロックに関する標準が提供されるだけでなく、半流動性を持つSAFTに特化した二次市場の創出も可能になります。

また、この規格はさまざまな種類のベスティングカーブを容易に実装することも可能です。

  • 線形ベスティング
  • クリフベスティング
  • 指数関数的ベスティング
  • カスタムな確定的ベスティング

線形ベスティング
ベスティング期間内でトークンを均等に段階的に解放する方法です。
以下に具体例を挙げて説明します。

例えば、あなたがプロジェクトに参加し、トークンを受け取ることが約束されているとします。
このプロジェクトでは、線形ベスティングが採用されており、ベスティング期間は1年間とします。
線形ベスティングでは、ベスティング期間内でトークンが均等に解放されるため、1年間でトークンの全量が解放されることになります。

具体的には、例えば1,200トークンを受け取ることが約束されている場合、1年間のベスティング期間を12ヶ月として、毎月100トークンずつ解放されるようになります。
最初の月には100トークン、2ヶ月目にも100トークン、そして12ヶ月目には最後の100トークンが解放されます。

これにより、時間の経過とともにトークンが段階的に解放されるため、プロジェクトへの参加者は長期間にわたってトークンを受け取ることができます。
線形ベスティングは、トークンの価値の安定的な増加を期待する場合や、トークンの長期的な関与を奨励する場合に使用されることがあります。

クリフベスティング
ある一定期間が経過するまでトークンを解放せず、その期間が経過した後に一度に全量を解放する方法です。
以下に具体例を挙げて説明します。

例えば、あなたが新しいプロジェクトの一員として参加し、トークンを受け取ることが約束されています。
このプロジェクトでは、クリフベスティングが採用されており、ベスティング期間は1年間、クリフ(待機期間)は6ヶ月とします。
これはつまり、最初の6ヶ月間はトークンが一切解放されず、その後に一度に全量が解放される仕組みです。

具体的には、例えば1,000トークンを受け取ることが約束されている場合、最初の6ヶ月間はトークンが解放されず、その後の6ヶ月目に一度に1,000トークンが解放されます。
つまり、6ヶ月間我慢してトークンを受け取りませんが、その後に一度に全量を手に入れることができる仕組みです。

クリフベスティングは、プロジェクトに参加者に一定のコミットメントを求めたり、将来の成功に対する期待感を高めるために使用されることがあります。
クリフ期間中はトークンが解放されないため、プロジェクトに本格的に取り組む意思を持つ参加者が増える効果があります。
その後、一度に全量が解放されることで、参加者はプロジェクトの成功により大きな報酬を得ることができるようになります。

指数関数的ベスティング
時間の経過に応じてトークンの解放が急速に増加する方法です。
以下に具体例を挙げて説明します。

例えば、あなたが新しいプロジェクトに参加し、トークンを受け取ることが約束されています。
このプロジェクトでは、指数関数的ベスティングが採用されており、ベスティング期間は1年間とします。
この場合、指数関数的ベスティングではトークンの解放が急速に増加するため、初めのうちは少ないが時間が経つにつれて急激に増加するパターンとなります。

具体的には、例えば1,000トークンを受け取ることが約束されている場合、最初の数ヶ月は解放されるトークンが少なく、例えば初月は10トークン、2ヶ月目は20トークンといったように増加します。
しかし、時間が経過するにつれて指数的に増加し、ベスティング期間が終わる頃には急激に増加して例えば1ヶ月に200トークンといった具合になります。

このように、指数関数的ベスティングは最初は少ないトークンの解放から始まり、徐々に増加し、最終的に急激に増加する特性を持っています。
この方法は、プロジェクトに長期的なコミットメントを示すメンバーや投資家に対して、将来的な成功に対する期待感を高めるために使用されることがあります。

カスタムな確定的ベスティング
独自のスケジュールに従ってトークンを解放する方法です
これにより、プロジェクトや投資家のニーズに合わせて柔軟なベスティングスケジュールを設定できます。
以下に具体例を挙げて説明します。

例えば、あなたが新しいプロジェクトに参加し、特定の条件に基づいてトークンを受け取ることが約束されています。
このプロジェクトでは、カスタムな確定的ベスティングが採用されており、独自のベスティングスケジュールが設定されています。

具体的には、例えば1,000トークンを受け取ることが約束されている場合、プロジェクトや投資家との合意に基づいて、トークンの解放スケジュールを設定することができます。
例えば、最初の3ヶ月間は解放されないが、その後毎月100トークンずつ解放されるといったカスタムなスケジュールを設定することができます。

別の例として、プロジェクトのマイルストーンの達成に応じてトークンを解放するスケジュールを設定することも考えられます。
例えば、特定の開発目標が達成されるごとにトークンを解放するようなスケジュールを設定することで、プロジェクトの進捗に応じてトークンを受け取ることができるようになります。

カスタムな確定的ベスティングは、特定のニーズや状況に合わせてトークンの解放スケジュールを柔軟に調整できるため、個々のプロジェクトや投資家に合わせたベスティングを実現するために使用されます。

これにより、柔軟なベスティングスケジュールを採用することができるようになります。

ユースケース

べスティングフレームワーク

トークンを一定期間にわたって段階的に解放する方法を提供し、これを使って債券や財務省短期証券などのさまざまなNFT金融商品を作成できます。

SAFT契約を半流動のベスティングNFTアセットとして標準的な形で再現

SAFTは通常オフチェーンで行われますが、現在のオンチェーンバージョンはアドレスベースが主流です。
これにより、多数の代表者にベスティングシェアを分配するのが難しい状況を改善し、標準化によって複雑なプロセスを簡素化します。

ベスティングやトークンタイムロックコントラクトの標準化

これらのコントラクトは多種多様であり、インターフェースや実装が異なります。

ベスティングNFT専用のNFTマーケットプレイスの提供

トークンベスティングNFTの共通標準に基づいて、新しいインターフェースや分析ツールを開発することができます。

ベスティングNFTを別サービスに統合

標準化により、Safe Walletのようなサービスがこれらのコントラクトとの連携をスムーズにかつ統一的にサポートできるようになります。

透明化と資金調達

標準化により、ベスティングトークン(例:SAFT)の透明な販売手法や、より一般的な資金調達の実装が可能になります。

べスティング情報の公開

ツール、フロントエンドアプリ、集約ツールなどが、ユーザーにベスティングトークンとそのプロパティの全体像を表示することができるようになります。
現在、プロジェクトごとにベスティングスケジュールの可視化を行う必要がありますが、標準化により、サードパーティツールがユーザーに対して異なるプロジェクトのベスティングNFTを集約表示し、スケジュールを表示し、集約したベスティングアクションを実行する手助けを行えるようになります。

ERC165のサポート

ツールは、ERC165の**supportsInterface(InterfaceID)**チェックを使用して簡単に準拠性を確認できます。
さらに、複数の受取人の定義やベスティングトークンの定期的な賃貸など、異なるベスティング標準に対して単一のラッピング実装を適用できるようになります。

仕様

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/**
 * @title Non-Fungible Vesting Token Standard.
 * @notice A non-fungible token standard used to vest ERC-20 tokens over a vesting release curve 
 *  scheduled using timestamps.
 * @dev Because this standard relies on timestamps for the vesting schedule, it's important to keep track of the 
 *  tokens claimed per Vesting NFT so that a user cannot withdraw more tokens than allotted for a specific Vesting NFT.
 * @custom:interface-id 0xbd3a202b
 */
interface IERC5725 is IERC721 {
    /**
     *  This event is emitted when the payout is claimed through the claim function.
     *  @param tokenId the NFT tokenId of the assets being claimed.
     *  @param recipient The address which is receiving the payout.
     *  @param claimAmount The amount of tokens being claimed.
     */
    event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 claimAmount);

    /**
     *  This event is emitted when an `owner` sets an address to manage token claims for all tokens.
     *  @param owner The address setting a manager to manage all tokens.
     *  @param spender The address being permitted to manage all tokens.
     *  @param approved A boolean indicating whether the spender is approved to claim for all tokens.
     */
    event ClaimApprovalForAll(address indexed owner, address indexed spender, bool approved);

    /**
     *  This event is emitted when an `owner` sets an address to manage token claims for a `tokenId`.
     *  @param owner The `owner` of `tokenId`.
     *  @param spender The address being permitted to manage a tokenId.
     *  @param tokenId The unique identifier of the token being managed.
     *  @param approved A boolean indicating whether the spender is approved to claim for `tokenId`.
     */
    event ClaimApproval(address indexed owner, address indexed spender, uint256 indexed tokenId, bool approved);

    /**
     * @notice Claim the pending payout for the NFT.
     * @dev MUST grant the claimablePayout value at the time of claim being called to `msg.sender`. 
     *  MUST revert if not called by the token owner or approved users. 
     *  MUST emit PayoutClaimed. 
     *  SHOULD revert if there is nothing to claim.
     * @param tokenId The NFT token id.
     */
    function claim(uint256 tokenId) external;

    /**
     * @notice Number of tokens for the NFT which have been claimed at the current timestamp.
     * @param tokenId The NFT token id.
     * @return payout The total amount of payout tokens claimed for this NFT.
     */
    function claimedPayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice Number of tokens for the NFT which can be claimed at the current timestamp.
     * @dev It is RECOMMENDED that this is calculated as the `vestedPayout()` subtracted from `payoutClaimed()`.
     * @param tokenId The NFT token id.
     * @return payout The amount of unlocked payout tokens for the NFT which have not yet been claimed.
     */
    function claimablePayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice Total amount of tokens which have been vested at the current timestamp. 
     *  This number also includes vested tokens which have been claimed.
     * @dev It is RECOMMENDED that this function calls `vestedPayoutAtTime` 
     *  with `block.timestamp` as the `timestamp` parameter.
     * @param tokenId The NFT token id.
     * @return payout Total amount of tokens which have been vested at the current timestamp.
     */
    function vestedPayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice Total amount of vested tokens at the provided timestamp. 
     *  This number also includes vested tokens which have been claimed.
     * @dev `timestamp` MAY be both in the future and in the past. 
     *  Zero MUST be returned if the timestamp is before the token was minted.
     * @param tokenId The NFT token id.
     * @param timestamp The timestamp to check on, can be both in the past and the future.
     * @return payout Total amount of tokens which have been vested at the provided timestamp.
     */
    function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) external view returns (uint256 payout);

    /**
     * @notice Number of tokens for an NFT which are currently vesting.
     * @dev The sum of vestedPayout and vestingPayout SHOULD always be the total payout.
     * @param tokenId The NFT token id.
     * @return payout The number of tokens for the NFT which are vesting until a future date.
     */
    function vestingPayout(uint256 tokenId) external view returns (uint256 payout);

    /**
     * @notice The start and end timestamps for the vesting of the provided NFT. 
     *  MUST return the timestamp where no further increase in vestedPayout occurs for `vestingEnd`.
     * @param tokenId The NFT token id.
     * @return vestingStart The beginning of the vesting as a unix timestamp.
     * @return vestingEnd The ending of the vesting as a unix timestamp.
     */
    function vestingPeriod(uint256 tokenId) external view returns (uint256 vestingStart, uint256 vestingEnd);

    /**
     * @notice Token which is used to pay out the vesting claims.
     * @param tokenId The NFT token id.
     * @return token The token which is used to pay out the vesting claims.
     */
    function payoutToken(uint256 tokenId) external view returns (address token);

    /**
     * @notice Sets a global `operator` with permission to manage all tokens owned by the current `msg.sender`.
     * @param operator The address to let manage all tokens.
     * @param approved A boolean indicating whether the spender is approved to claim for all tokens.
     */
    function setClaimApprovalForAll(address operator, bool approved) external;

    /**
     * @notice Sets a tokenId `operator` with permission to manage a single `tokenId` owned by the `msg.sender`.
     * @param operator The address to let manage a single `tokenId`.
     * @param tokenId the `tokenId` to be managed.
     * @param approved A boolean indicating whether the spender is approved to claim for all tokens.
     */
    function setClaimApproval(address operator, bool approved, uint256 tokenId) external;

    /**
     * @notice Returns true if `owner` has set `operator` to manage all `tokenId`s.
     * @param owner The owner allowing `operator` to manage all `tokenId`s.
     * @param operator The address who is given permission to spend tokens on behalf of the `owner`.
     */
    function isClaimApprovedForAll(address owner, address operator) external view returns (bool isClaimApproved);

    /**
     * @notice Returns the operating address for a `tokenId`. 
     *  If `tokenId` is not managed, then returns the zero address.
     * @param tokenId The NFT `tokenId` to query for a `tokenId` manager.
     */
    function getClaimApproved(uint256 tokenId) external view returns (address operator);
}

補足

条件

ベスティング(vesting)

ベスティングNFTが将来の特定の日付まで解放されるトークンのことを指します。

ベスティング済み(vested)

ベスティングNFTがすでに解放されたトークンの総量です。

請求可能(claimable)

ベスティングNFTが解放可能なベスティング済みトークンの量を示します。

請求済み(claimed)

ベスティングNFTから解放されたトークンの合計量です。

タイムスタンプ(timestamp)

ベスティングの日付を表すために使用されるUnixタイムスタンプ(秒)です。

べスティング関数

vestingPayoutとvestedPayout

vestingPayout(uint256 tokenId)vestedPayout(uint256 tokenId)は、ベスティングスケジュールの終了までに請求できるトークンの総量の合計です。
つまり、NFTがベスティング中のトークンの総量を表します。
また、type(uint256).maxtimestampとして使用した場合、vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)と同じ結果になります。

これは、ベスティング中のトークンとベスティングがすべて正確に追跡されていることを保証するためのものです。
ベスティングカーブはベスティング期間全体で決定的であることを意図しており、これによりNFTとの統合が容易になります。
例えば、ベスティングスケジュールを反復処理して、ベスティングカーブをオンチェーンまたはオフチェーンで視覚化することができます。

vestedPayoutとclaimedPayout、claimablePayout

vestedPayout - claimedPayout - claimablePayout = lockedPayout

vestedPayout(uint256 tokenId)は、claimedPayout(uint256 tokenId)を含むベスティングされた支払いトークンの総額を提供します。
claimedPayout(uint256 tokenId)は、現在のタイムスタンプでロックが解除された支払いトークンの合計量を示します。
一方、claimablePayout(uint256 tokenId)は、現在のタイムスタンプでロックが解除できる支払いトークンの量を示します。

これらの関数を提供する目的は次の通りです。

  1. vestedPayout(uint256 tokenId)の結果は、block.timestamptimestampとして使用したvestedPayoutAtTime(uint256 tokenId, uint256 timestamp)と常に一致します。
  2. claimablePayout(uint256 tokenId)は、現在のアンロック可能な支払い量を簡単に確認でき、特定のタイムスタンプまではアンロックが行われないようにゼロを返すことでクリフを設定できます。
  3. claimedPayout(uint256 tokenId)は、NFTからアンロックされたトークンの量を表示するだけでなく、ベスティングされたがまだロックされた支払いトークンの計算にも必要です(vestedPayout - claimedPayout - claimablePayout = lockedPayout)
    これは、標準の実装によってベスティングカーブがどのように構成されるかに依存します。

vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)は、vestingPeriod(uint256 tokenId)を反復処理し、リリースカーブを視覚化する機能を提供します。
リリースカーブはベスティング期間内で決定論的であることを意図しています。

タイムスタンプ

Solidityの開発では、通常、ブロックのタイムスタンプ(block.timestamp)を状態に依存する変数として使用することは推奨されません。
なぜなら、このタイムスタンプはマイナーによって操作される可能性があるためです。
しかし、この規格ではブロックに対するタイムスタンプを使用する代わりに、Ethereum Virtual Machine(EVM)互換ネットワーク間で一貫性を保つためにタイムスタンプを採用しています。
異なるネットワークは通常異なるブロックタイムを持っており、大幅に操作されたタイムスタンプを持つブロック提案は、ノードの実装によって破棄されるため、悪用の可能性は極めて低いです。

タイムスタンプの使用により、異なるチェーン間での統合が容易になります。
しかし、内部的には、各Vesting NFTについて余剰のトークンがベスティング条件で割り当てられた量を超えて請求されないように、トークンの支払いを追跡しています。

スコープの制限

過去の請求

過去のベスティングスケジュールは、vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)を使用してチェーン上で特定できますが、過去の請求に関しては、過去のトランザクションデータを使用して計算する必要があります。
PayoutClaimedイベントをクエリして過去の請求データを収集し、その情報から過去の請求グラフを構築することになります。

拡張の可能性

これらの機能は、現在の標準仕様には含まれていませんが、標準を拡張することでこれらの高度な機能をサポートすることができます。

カスタムなベスティングカーブ

この標準は、NFTのtokenIdとタイムスタンプを入力として与えることで、決定論的なベスティング値を返すことを意図しています。
これにより、ベスティングカーブの内部動作における柔軟性が保たれ、複雑なスマートコントラクトのベスティングアーキテクチャを構築するプロジェクトが制約なく取り組むことができます。

NFTのレンタル

ベスティングNFTをレンタルできる仕組みが提供されれば、さらに複雑なDeFiツールを開発することが可能です。

これらの機能は、基本的な標準を簡潔に保つために現在はサポートされていませんが、将来的にはこの標準を拡張することで追加される可能性があります。

後方互換性

互換性の確保

Vesting NFT規格は、現行のERC721の統合とマーケットプレイスと完全に互換性を持つように設計されています。
つまり、既存のERC721トークンを問題なくこの規格に統合することができ、既存のマーケットプレイスでも使用することができます。

ERC165インターフェースのサポート

また、Vesting NFT規格は、ERC165インターフェースの検出をサポートしています。
これにより、EIP721の互換性だけでなく、Vesting NFTの互換性も検出できます。
*ERC165は、コントラクトが特定のインターフェースをサポートしているかどうかを確認するための標準的な方法を提供します。

参考実装

以下に格納しています。

引用: https://eips.ethereum.org/assets/eip-5725/

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "./IERC5725.sol";

abstract contract ERC5725 is IERC5725, ERC721 {
    using SafeERC20 for IERC20;

    /// @dev mapping for claimed payouts
    mapping(uint256 => uint256) /*tokenId*/ /*claimed*/ internal _payoutClaimed;

    /// @dev Mapping from token ID to approved tokenId operator
    mapping(uint256 => address) private _tokenIdApprovals;

    /// @dev Mapping from owner to operator approvals
    mapping(address => mapping(address => bool)) /* owner */ /*(operator, isApproved)*/ internal _operatorApprovals;

    /**
     * @notice Checks if the tokenId exists and its valid
     * @param tokenId The NFT token id
     */
    modifier validToken(uint256 tokenId) {
        require(_exists(tokenId), "ERC5725: invalid token ID");
        _;
    }

    /**
     * @dev See {IERC5725}.
     */
    function claim(uint256 tokenId) external override(IERC5725) validToken(tokenId) {
        require(isApprovedClaimOrOwner(msg.sender, tokenId), "ERC5725: not owner or operator");

        uint256 amountClaimed = claimablePayout(tokenId);
        require(amountClaimed > 0, "ERC5725: No pending payout");

        emit PayoutClaimed(tokenId, msg.sender, amountClaimed);

        _payoutClaimed[tokenId] += amountClaimed;
        IERC20(payoutToken(tokenId)).safeTransfer(msg.sender, amountClaimed);
    }

    /**
     * @dev See {IERC5725}.
     */
    function setClaimApprovalForAll(address operator, bool approved) external override(IERC5725) {
        _setClaimApprovalForAll(operator, approved);
        emit ClaimApprovalForAll(msg.sender, operator, approved);
    }

    /**
     * @dev See {IERC5725}.
     */
    function setClaimApproval(
        address operator,
        bool approved,
        uint256 tokenId
    ) external override(IERC5725) validToken(tokenId) {
        _setClaimApproval(operator, tokenId);
        emit ClaimApproval(msg.sender, operator, tokenId, approved);
    }

    /**
     * @dev See {IERC5725}.
     */
    function vestedPayout(uint256 tokenId) public view override(IERC5725) returns (uint256 payout) {
        return vestedPayoutAtTime(tokenId, block.timestamp);
    }

    /**
     * @dev See {IERC5725}.
     */
    function vestedPayoutAtTime(
        uint256 tokenId,
        uint256 timestamp
    ) public view virtual override(IERC5725) returns (uint256 payout);

    /**
     * @dev See {IERC5725}.
     */
    function vestingPayout(
        uint256 tokenId
    ) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
        return _payout(tokenId) - vestedPayout(tokenId);
    }

    /**
     * @dev See {IERC5725}.
     */
    function claimablePayout(
        uint256 tokenId
    ) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
        return vestedPayout(tokenId) - _payoutClaimed[tokenId];
    }

    /**
     * @dev See {IERC5725}.
     */
    function claimedPayout(
        uint256 tokenId
    ) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
        return _payoutClaimed[tokenId];
    }

    /**
     * @dev See {IERC5725}.
     */
    function vestingPeriod(
        uint256 tokenId
    ) public view override(IERC5725) validToken(tokenId) returns (uint256 vestingStart, uint256 vestingEnd) {
        return (_startTime(tokenId), _endTime(tokenId));
    }

    /**
     * @dev See {IERC5725}.
     */
    function payoutToken(uint256 tokenId) public view override(IERC5725) validToken(tokenId) returns (address token) {
        return _payoutToken(tokenId);
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     * IERC5725 interfaceId = 0xbd3a202b
     */
    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override(ERC721, IERC165) returns (bool supported) {
        return interfaceId == type(IERC5725).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC5725}.
     */
    function getClaimApproved(uint256 tokenId) public view returns (address operator) {
        return _tokenIdApprovals[tokenId];
    }

    /**
     * @dev Returns true if `owner` has set `operator` to manage all `tokenId`s.
     * @param owner The owner allowing `operator` to manage all `tokenId`s.
     * @param operator The address who is given permission to spend tokens on behalf of the `owner`.
     */
    function isClaimApprovedForAll(address owner, address operator) public view returns (bool isClaimApproved) {
        return _operatorApprovals[owner][operator];
    }

    /**
     * @dev Public view which returns true if the operator has permission to claim for `tokenId`
     * @notice To remove permissions, set operator to zero address.
     *
     * @param operator The address that has permission for a `tokenId`.
     * @param tokenId The NFT `tokenId`.
     */
    function isApprovedClaimOrOwner(address operator, uint256 tokenId) public view virtual returns (bool) {
        address owner = ownerOf(tokenId);
        return (operator == owner || isClaimApprovedForAll(owner, operator) || getClaimApproved(tokenId) == operator);
    }

    /**
     * @dev Internal function to set the operator status for a given owner to manage all `tokenId`s.
     * @notice To remove permissions, set approved to false.
     *
     * @param operator The address who is given permission to spend vested tokens.
     * @param approved The approved status.
     */
    function _setClaimApprovalForAll(address operator, bool approved) internal virtual {
        _operatorApprovals[msg.sender][operator] = approved;
    }

    /**
     * @dev Internal function to set the operator status for a given tokenId.
     * @notice To remove permissions, set operator to zero address.
     *
     * @param operator The address who is given permission to spend vested tokens.
     * @param tokenId The NFT `tokenId`.
     */
    function _setClaimApproval(address operator, uint256 tokenId) internal virtual {
        require(ownerOf(tokenId) == msg.sender, "ERC5725: not owner of tokenId");
        _tokenIdApprovals[tokenId] = operator;
    }

    /**
     * @dev Internal function to hook into {IERC721-_afterTokenTransfer}, when a token is being transferred.
     * Removes permissions to _tokenIdApprovals[tokenId] when the tokenId is transferred, burnt, but not on mint.
     *
     * @param from The address from which the tokens are being transferred.
     * @param to The address to which the tokens are being transferred.
     * @param firstTokenId The first tokenId in the batch that is being transferred.
     * @param batchSize The number of tokens being transferred in this batch.
     */
    function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override {
        super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
        if (from != address(0)) {
            delete _tokenIdApprovals[firstTokenId];
        }
    }

    /**
     * @dev Internal function to get the payout token of a given vesting NFT
     *
     * @param tokenId on which to check the payout token address
     * @return address payout token address
     */
    function _payoutToken(uint256 tokenId) internal view virtual returns (address);

    /**
     * @dev Internal function to get the total payout of a given vesting NFT.
     * @dev This is the total that will be paid out to the NFT owner, including historical tokens.
     *
     * @param tokenId to check
     * @return uint256 the total payout of a given vesting NFT
     */
    function _payout(uint256 tokenId) internal view virtual returns (uint256);

    /**
     * @dev Internal function to get the start time of a given vesting NFT
     *
     * @param tokenId to check
     * @return uint256 the start time in epoch timestamp
     */
    function _startTime(uint256 tokenId) internal view virtual returns (uint256);

    /**
     * @dev Internal function to get the end time of a given vesting NFT
     *
     * @param tokenId to check
     * @return uint256 the end time in epoch timestamp
     */
    function _endTime(uint256 tokenId) internal view virtual returns (uint256);
}

_payoutClaimed

mapping(uint256 => uint256) internal _payoutClaimed;

概要
NFTトークンのIDごとにクレーム済みの支払い額を保持するマッピング配列。

詳細
NFTトークンのIDをキーとし、そのトークンに対してクレームされた支払い額(uint256)を値として持ちます。
トークンの所有者が支払いをクレームする時に使用されます。

パラメータ

  • tokenId
    • NFTトークンのID。

_tokenIdApprovals

mapping(uint256 => address) private _tokenIdApprovals;

概要
NFTトークンのIDごとに送付権限が承認されたアドレスを保持するためのマッピングです。

詳細
NFTトークンのIDをキーとし、そのトークンに対して送付権限が承認されたアドレスを値として保持します。
トークンの所有者が特定のアドレスにトークンの権限を付与する場合に使用されます。

パラメータ

  • tokenId
    • NFTトークンのID。

_operatorApprovals

mapping(address => mapping(address => bool)) internal _operatorApprovals;

概要
所有者と送付などを承認されたアドレスの間での権限の承認状況を保持するマッピング配列。

詳細
所有者のアドレスを最初のキー、権限を付与されたアドレスを二次のキーとして使用し、その所有者が権限が付与された特定のアドレスに対して権限を承認しているかどうか(bool)を値として持ちます。
所有者は、アドレスに対してトークンの転送や操作を許可する時に使用されます。

パラメータ

  • owner
    • NFTトークンの所有者のアドレス。
  • operator
    • 操作者のアドレス。

validToken

modifier validToken(uint256 tokenId) {
    require(_exists(tokenId), "ERC5725: invalid token ID");
    _;
}

概要
この修飾子は、トークンIDが存在し、有効であるかどうかを確認する修飾子。

詳細
関数の実行前にトークンの存在を確認し、無効なトークンIDの場合はエラーメッセージを表示します。
トークンの操作やクレームの際に、事前にトークンの有効性を確認するために利用されます。

パラメータ

  • tokenId
    • 確認するNFTトークンのID。

claim

function claim(uint256 tokenId) external override(IERC5725) validToken(tokenId) {
    require(isApprovedClaimOrOwner(msg.sender, tokenId), "ERC5725: not owner or operator");

    uint256 amountClaimed = claimablePayout(tokenId);
    require(amountClaimed > 0, "ERC5725: No pending payout");

    emit PayoutClaimed(tokenId, msg.sender, amountClaimed);

    _payoutClaimed[tokenId] += amountClaimed;
    IERC20(payoutToken(tokenId)).safeTransfer(msg.sender, amountClaimed);
}

概要
特定のトークンIDに対してクレームを行う関数。

詳細
所有者またはオペレーターが呼び出し、保留中の支払いがある場合、指定されたトークンIDに対してクレームを行います。
支払い可能な額が0より大きいかチェックします。
その後、PayoutClaimedイベントが発行され、クレームされた額が送信者に送られます。

引数

  • tokenId
    • クレームを行う対象のトークンID。

setClaimApprovalForAll

function setClaimApprovalForAll(address operator, bool approved) external override(IERC5725) {
    _setClaimApprovalForAll(operator, approved);
    emit ClaimApprovalForAll(msg.sender, operator, approved);
}

概要
オペレーターに対するクレームの承認を設定する関数。

詳細
呼び出し者がオペレーターに対するクレームの承認を設定または解除します。
操作が完了すると、ClaimApprovalForAllイベントが発行されます。

引数

  • operator
    • クレームの操作を許可するオペレーターのアドレス。
  • approved
    • 承認するかどうかのブール値。

setClaimApproval

function setClaimApproval(
    address operator,
    bool approved,
    uint256 tokenId
) external override(IERC5725) validToken(tokenId) {
    _setClaimApproval(operator, tokenId);
    emit ClaimApproval(msg.sender, operator, tokenId, approved);
}

概要
特定のトークンIDに対するクレームの承認を設定する関数。

詳細
所有者が特定のトークンIDに対するクレームの承認を設定または解除します。
操作が完了すると、ClaimApprovalイベントが発行されます。

引数

  • operator
    • クレームの操作を許可するオペレーターのアドレス。
  • approved
    • 承認するかどうかのブール値。
  • tokenId
    • クレームの対象となるトークンID。

vestedPayout

function vestedPayout(uint256 tokenId) public view override(IERC5725) returns (uint256 payout) {
    return vestedPayoutAtTime(tokenId, block.timestamp);
}

概要
特定のトークンIDに対するベステッド(配当権付与)支払い額を取得する関数。

詳細
指定されたトークンIDに対して、ブロックのタイムスタンプに基づいてベステッド支払い額を計算します。

引数

  • tokenId
    • ベステッド支払い額を取得する対象のトークンID。

戻り値

  • payout
    • ベステッド支払い額。

vestedPayoutAtTime

function vestedPayoutAtTime(
    uint256 tokenId,
    uint256 timestamp
) public view virtual override(IERC5725) returns (uint256 payout);

概要
特定のトークンIDに対する指定したタイムスタンプでのベステッド支払い額を取得する関数。

詳細
指定されたトークンIDに対して、指定したタイムスタンプでのベステッド支払い額を計算して返します。

引数

  • tokenId
    • ベステッド支払い額を取得する対象のトークンID。
  • timestamp
    • タイムスタンプ。

戻り値

  • payout
    • ベステッド支払い額。

vestingPayout

function vestingPayout(
    uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
    return _payout(tokenId) - vestedPayout(tokenId);
}

概要
特定のトークンIDに対するベスティング(配当権付与)期間内の支払い額を取得する関数。

引数

  • tokenId
    • ベスティング期間内の支払い額を表示する対象のトークンID。

戻り値

  • payout
    • ベスティング期間内の支払い額。

claimablePayout

function claimablePayout(
    uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
    return vestedPayout(tokenId) - _payoutClaimed[tokenId];
}

概要
特定のトークンIDに対してまだクレームされていない支払い額を取得する関数。

引数

  • tokenId
    • クレーム可能な支払い額を取得する対象のトークンID。

戻り値

  • payout
    • クレーム可能な支払い額。

claimedPayout

function claimedPayout(
    uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
    return _payoutClaimed[tokenId];
}

概要
特定のトークンIDに対してクレーム済みの支払い額を取得する関数。

引数

  • tokenId
    • クレーム済みの支払い額を取得する対象のトークンID。

戻り値

  • payout
    • クレーム済みの支払い額。

vestingPeriod

function vestingPeriod(
    uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 vestingStart, uint256 vestingEnd) {
    return (_startTime(tokenId), _endTime(tokenId));
}

概要
特定のトークンIDに対するベスティング期間を取得する関数。

詳細
指定されたトークンIDのベスティング期間の開始と終了のタイムスタンプを取得します。

引数

  • tokenId
    • ベスティング期間を取得する対象のトークンID。

戻り値

  • vestingStart
    • ベスティング期間の開始のタイムスタンプ。
  • vestingEnd
    • ベスティング期間の終了のタイムスタンプ。

payoutToken

function payoutToken(uint256 tokenId) public view override(IERC5725) validToken(tokenId) returns (address token) {
    return _payoutToken(tokenId);
}

概要
特定のトークンIDに対する支払いトークンのアドレスを取得する関数。

引数

  • tokenId
    • 支払いトークンのアドレスを取得する対象のトークンID。

戻り値

  • token
    • 支払いトークンのアドレス。

supportsInterface

function supportsInterface(
    bytes4 interfaceId
) public view virtual override(ERC721, IERC165) returns (bool supported) {
    return interfaceId == type(IERC5725).interfaceId || super.supportsInterface(interfaceId);
}

概要
指定されたインターフェースがサポートされているかどうかを示す関数。

詳細
指定されたインターフェースがIERC5725のインターフェースIDと一致するか、親クラスのsupportsInterface関数を呼び出してインターフェースのサポートを確認します。

引数

  • interfaceId
    • インターフェースのID。

戻り値

  • supported
    • インターフェースがサポートされているかどうかのブール値。

getClaimApproved

function getClaimApproved(uint256 tokenId) public view returns (address operator) {
    return _tokenIdApprovals[tokenId];
}

概要
特定のトークンIDに対するクレームの承認オペレーターを取得する関数。

引数

  • tokenId
    • クレームの承認オペレーターを取得する対象のトークンID。

戻り値

  • operator
    • クレームの承認オペレーターのアドレス。

isClaimApprovedForAll

function isClaimApprovedForAll(address owner, address operator) public view returns (bool isClaimApproved) {
    return _operatorApprovals[owner][operator];
}

概要
所有者がオペレーターに対してクレームの承認を設定しているかどうかを取得する関数。

引数

  • owner
    • 所有者のアドレス。
  • operator
    • クレームの承認オペレーターのアドレス。

戻り値

  • isClaimApproved
    • クレームの承認が設定されているかどうかのブール値。

isApprovedClaimOrOwner

function isApprovedClaimOrOwner(address operator, uint256 tokenId) public view virtual returns (bool) {
    address owner = ownerOf(tokenId);
    return (operator == owner || isClaimApprovedForAll(owner, operator) || getClaimApproved(tokenId) == operator);
}

概要
指定されたオペレーターがトークンのクレームを行うことができるかどうかを取得する関数。

詳細
指定されたオペレーターがトークンの所有者であるか、トークンの所有者がクレームの承認オペレーターに設定しているか、トークンのクレームの承認オペレーターと一致する場合、クレームが許可されていると判断します。

引数

  • operator
    • クレームを行うオペレーターのアドレス。
  • tokenId
    • クレームを行う対象のトークンID。

戻り値

  • bool
    • クレームが許可されているかどうかのブール値。

_setClaimApprovalForAll

function _setClaimApprovalForAll(address operator, bool approved) internal virtual {
    _operatorApprovals[msg.sender][operator] = approved;
}

概要
所有者によって指定されたオペレーターに対するクレームの承認を行う関数。

詳細
所有者が指定されたオペレーターに対するクレームの承認を設定または解除します。

引数

  • operator
    • クレームの操作を許可するオペレーターのアドレス。
  • approved
    • 承認するかどうかのブール値。

_setClaimApproval

function _setClaimApproval(address operator, uint256 tokenId) internal virtual {
    require(ownerOf(tokenId) == msg.sender, "ERC5725: not owner of tokenId");
    _tokenIdApprovals[tokenId] = operator;
}

概要
トークンの所有者によって指定されたトークンIDに対するクレームの承認を設定する関数。

詳細
トークンの所有者が指定されたトークンIDに対するクレームの承認を設定または解除します。

引数

  • operator
    • クレームの操作を許可するオペレーターのアドレス。
  • tokenId
    • クレームの対象となるトークンID。

_beforeTokenTransfer

function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override {
    super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
    if (from != address(0)) {
        delete _tokenIdApprovals[firstTokenId];
    }
}

概要
トークンが転送される前に呼び出される関数。

詳細
トークンが転送される前に、指定されたトークンIDに対するクレームの承認をリセットします。
これは、トークンが転送、破棄される時に実行されます。

引数

  • from
    • トークンが転送される元のアドレス。
  • to
    • トークンが転送される先のアドレス。
  • firstTokenId
    • バッチ内の最初のトークンID。
  • batchSize
    • 転送されるトークンのバッチサイズ。

_payoutToken

function _payoutToken(uint256 tokenId) internal view virtual returns (address)

概要
指定されたベスティング(配当権付与)NFTの支払いトークンのアドレスを取得する関数。

詳細
指定されたトークンIDに対するベスティングNFTの支払いトークンのアドレスを取得します。

引数

  • tokenId
    • 支払いトークンを取得する対象のトークンID。

戻り値

  • address
    • 支払いトークンのアドレス。

_payout

function _payout(uint256 tokenId) internal view virtual returns (uint256)

概要
指定されたベスティングNFTのトータル支払い額を取得する関数。

詳細
指定されたトークンIDに対するベスティングNFTのトータル支払い額を取得します。
これには過去の支払いも含まれます。

引数

  • tokenId
    • トータル支払い額を取得する対象のトークンID。

戻り値

  • uint256
    • ベスティングNFTのトータル支払い額。

_startTime

function _startTime(uint256 tokenId) internal view virtual returns (uint256)

概要
指定されたベスティングNFTの開始時刻を取得する関数。

詳細
指定されたトークンIDに対するベスティングNFTの開始時刻をエポックタイムスタンプで取得します。

引数

  • tokenId
    • 開始時刻を取得する対象のトークンID。

戻り値

  • uint256
    • ベスティングNFTの開始時刻(エポックタイムスタンプ)。

_endTime

function _endTime(uint256 tokenId) internal view virtual returns (uint256)

概要
指定されたベスティングNFTの終了時刻を取得する関数。

詳細
指定されたトークンIDに対するベスティングNFTの終了時刻をエポックタイムスタンプで取得します。

引数

  • tokenId
    • 終了時刻を取得する対象のトークンID。

戻り値

  • uint256
    • ベスティングNFTの終了時刻(エポックタイムスタンプ)。

セキュリティ考慮事項

タイムスタンプ

ベスティングスケジュールはタイムスタンプに基づいています。
これにより、将来の特定の日付までにベスティングされるトークンの数量を管理できます。
しかし、注意が必要です。
請求されたトークンの総量が、ベスティングNFTに割り当てられた量を超えないように気を付ける必要があります。
たとえば、vestedPayoutAtTime(tokenId, type(uint256).max)は、特定のtokenIdに対する全体の支払い量を返すべきです。

承認

ERC721トークンの承認がベスティングNFTに与えられた場合、オペレーターはそのベスティングNFTを自分自身に移動させ、その後ベスティングされたトークンを請求する権利を持つことになります。
この権限を悪用されないように、適切なアクセス制御と監視が重要です。
また、ERC5725の承認がベスティングNFTに与えられた場合、オペレーターはベスティングされたトークンを請求する権利を持ちますが、NFTをオーナーから移動させる権利は持ちません。

これらのセキュリティに関する考慮事項は、ベスティングNFTの安全な運用とユーザーの資産保護のために非常に重要です。
十分な検討と対策を講じることで、不正なアクセスやトークンの不正な使用を防ぐことができます。

引用

Apeguru (@Apegurus), Marco De Vries marco@paladinsec.co, Mario mario@paladinsec.co, DeFiFoFum (@DeFiFoFum), Elliott Green (@elliott-green), "ERC-5725: Transferable Vesting NFT [DRAFT]," Ethereum Improvement Proposals, no. 5725, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5725.

最後に

今回は「NFTを使用して段階的にERC20トークンを解放するべスティングのインターフェースを提案するERC5725」についてまとめてきました!
いかがだったでしょうか?

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

Twitter @cardene777

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?