はじめに

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

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

今回は、NFTのtransferとapproveに新たな認証ステップを追加し、ウォレット内からの不正なNFTの流出を防ぐ仕組みを提案している規格であるERC6997についてまとめていきます!

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

6997は現在(2024年1月11日)では「Review」段階です。

他にも様々なEIPについてまとめています。

概要

この規格はERC721を拡張したもので、ウォレットのドレイン(資金の不正な流出)を防ぐために、新しい検証(バリデーション)機能を導入しています。 通常のERC721基準では、トークンの転送や承認(transferやapprove関数など)は即時に行われます。 しかし、この拡張基準では、これらの操作がすぐに実行されるのではなく、バリデーションを待つロック状態に置かれます。 つまり、トークンの移動や承認が行われる前に、何らかの検証プロセスを経なければなりません。 これにより、不正な取引や攻撃者による資金の流出を防ぐためのセキュリティレイヤーが追加されます。 このようなバリデーション機能は、スマートコントラクトによって実装され、異常な活動を検出したり、特定の条件が満たされるまでトークンの移動を保留することができます。

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

動機

ブロックチェーンの強みは、同時にその弱点でもあるとされています。 それは、ユーザーにデータの完全な責任を与えることです。 現在、NFTの盗難の事例が多く存在し、NFTをコールドウォレットに移すなどの防盗策がありますが、これによりNFTの利便性が損なわれています。

各transferやapproveの前にバリデーションステップを設けることで、スマートコントラクトの開発者は安全なNFT盗難対策を実装する機会を持ちます。 例えば、バリデータアドレスが全てのスマートコントラクトトランザクションの検証を担当するシステムです。 このアドレスはdAppに接続され、ユーザーは自分のNFTのバリデーションリクエストを確認し、正しいものを承認できます。

このアドレスにトランザクション検証のみの権限を与えることにより、悪意あるユーザーがNFTを盗むためには、ユーザーのアドレスとバリデータアドレスの両方を同時に掌握する必要があるためシステムはより安全になります。 このように、各操作の前にバリデーションステップを設けることで、よりセキュアなNFTの管理と利用が可能になるのです。

仕様

ERC721準拠のコントラクトは、このEIP(イーサリアム改善提案)を実装することが可能です。 このEIPでは、NFTの所有権を変更する操作(例えば transferFrom や safeTransferFrom)は、TransferValidation を保留状態で作成し、ValidateTransfer イベントを発行する必要がありますが、NFTの所有権を移転してはなりません。

また、NFTの管理権を承認する操作(例えば approve や setApprovalForAll)も、ApprovalValidation を保留状態で作成し、ValidateApproval イベントを発行する必要がありますが、承認を有効にしてはなりません。

承認されたアカウントから転送が呼び出された場合、所有者でなくてもバリデーションなしで直接実行する必要があります。 これは、NFTを直接動かすために承認を必要とする現在のマーケットプレイスに対応するためです。

TransferValidation または ApprovalValidation を検証する時には、validフィールドをtrueに設定し、再度検証してはならないとされています。

TransferValidation を検証する操作は、NFTの所有権を変更するか、または承認を有効にする必要があります。 ApprovalValidation を検証する操作は、承認を有効にする役割を果たします。

コントラクトインターフェース

javascript
interface IERC6997 {

    struct TransferValidation {
        // The address of the owner.
        address from;
        // The address of the receiver.
        address to;
        // The token Id.
        uint256 tokenId;
        // Whether is a valid transfer.
        bool valid;
    }

    struct ApprovalValidation {
        // The address of the owner.
        address owner;
        // The approved address.
        address approve;
        // The token Id.
        uint256 tokenId;
        // Wether is a total approvement.
        bool approveAll;
        // Whether is a valid approve.
        bool valid;
    }

    /**
     * @dev Emitted when a new transfer validation has been requested.
     */
    event ValidateTransfer(address indexed from, address to, uint256 indexed tokenId, uint256 indexed transferValidationId);

    /**
    * @dev Emitted when a new approval validation has been requested.
    */
    event ValidateApproval(address indexed owner, address approve, uint256 tokenId, bool indexed approveAll, uint256 indexed approvalValidationId);

    /**
     * @dev Returns true if this contract is a validator ERC721.
     */
    function isValidatorContract() external view returns (bool);

    /**
     * @dev Returns the transfer validation struct using the transfer ID.
     *
     */
    function transferValidation(uint256 transferId) external view returns (TransferValidation memory);

    /**
    * @dev Returns the approval validation struct using the approval ID.
    *
    */
    function approvalValidation(uint256 approvalId) external view returns (ApprovalValidation memory);

    /**
     * @dev Return the total amount of transfer validations created.
     *
     */
    function totalTransferValidations() external view returns (uint256);

    /**
     * @dev Return the total amount of transfer validations created.
     *
     */
    function totalApprovalValidations() external view returns (uint256);
}

TransferValidation

javascript
struct TransferValidation {
    address from;
    address to;
    uint256 tokenId;
    bool valid;
}

概要 NFTのtrasnferに関するバリデーション情報を格納する構造体。

詳細 この構造体は、NFTの転送を検証する際に使用されるデータのセットを定義します。 オーナーのアドレス、受取人のアドレス、トークンID、およびtransferの有効性を表します。

パラメータ

  • address from
    • ークンの現在のオーナーのアドレス。
  • address to
    • トークンを受け取るアドレス。
  • uint256 tokenId
    • トークンのID。
  • bool valid
    • transferが有効かどうかを示すbool値。

ApprovalValidation

javascript
struct ApprovalValidation {
    address owner;
    address approve;
    uint256 tokenId;
    bool approveAll;
    bool valid;
}

概要 NFTの承認に関するバリデーション情報を格納する構造体。

詳細 この構造体は、NFTの承認を検証する時に使用されるデータのセットを定義します。 オーナーのアドレス、承認されるアドレス、トークンID、全てのトークンに対する承認の有無、および承認の有効性を表します。

パラメータ

  • address owner
    • トークンのオーナーのアドレス。
  • address approve
    • 承認されるアドレス。
  • uint256 tokenId
    • トークンのID。
  • bool approveAll
    • 全てのトークンに対する承認を示すbool値。
  • bool valid
    • 承認が有効かどうかを示すbool値。

ValidateTransfer

javascript
event ValidateTransfer(address indexed from, address to, uint256 indexed tokenId, uint256 indexed transferValidationId);

概要 新しいtransferバリデーションが要求された時に発行されるイベント。

詳細 このイベントは、transferバリデーションの要求があった際にブロックチェーン上で記録され、関連する情報を提供します。

パラメータ

  • address indexed from
    • トークンの現在のオーナーのアドレス。
  • address to
    • トークンを受け取るアドレス。
  • uint256 indexed tokenId
    • トークンのID。
  • uint256 indexed transferValidationId
    • transferバリデーションのID。

ValidateApproval

javascript
event ValidateApproval(address indexed owner, address approve, uint256 tokenId, bool indexed approveAll, uint256 indexed approvalValidationId);

概要 新しい承認バリデーションが要求された時に発行されるイベント。

詳細 このイベントは、承認バリデーションの要求があった時にブロックチェーン上で記録され、関連する情報を提供します。

パラメータ

  • address indexed owner
    • トークンのオーナーのアドレス。
  • address approve
    • 承認されるアドレス。
  • uint256 tokenId
    • トークンのID。
  • bool indexed approveAll
    • 全てのトークンに対する承認を示すbool値。
  • uint256 indexed approvalValidationId
    • 承認バリデーションのID。

isValidatorContract

javascript
function isValidatorContract() external view returns (bool);

概要 この関数は、コントラクトがバリデータ ERC721 であるかどうかを確認します。

詳細 isValidatorContract 関数は、呼び出されたコントラクトがバリデーションを行うための特定の機能を持っているかどうかを判断するために使用されます。

戻り値

  • bool: コントラクトがバリデータ ERC721 である場合は true、そうでない場合は false を返します。

transferValidation

javascript
function transferValidation(uint256 transferId) external view returns (TransferValidation memory);

概要 特定の転送IDに関連する TransferValidation 構造体を返す関数。

詳細 transferValidation 関数は、指定された転送IDに対応する転送バリデーションデータを取得するために使用されます。

引数

  • uint256 transferId
    • 取得したい転送バリデーションのID。

戻り値

  • TransferValidation
    • 指定されたIDに関連する転送バリデーション情報。

approvalValidation

javascript
function approvalValidation(uint256 approvalId) external view returns (ApprovalValidation memory);

概要 特定の承認IDに関連する ApprovalValidation 構造体を返す関数。

詳細 approvalValidation 関数は、指定された承認IDに対応する承認バリデーションデータを取得するために使用されます。

引数

  • uint256 approvalId
    • 取得したい承認バリデーションのID。

戻り値

  • ApprovalValidation
    • 指定されたIDに関連する承認バリデーション情報。

totalTransferValidations

javascript
function totalTransferValidations() external view returns (uint256);

概要 作成された転送バリデーションの合計数を返す関数。

詳細 totalTransferValidations 関数は、これまでに作成されたすべての転送バリデーションの数を返すために使用されます。

戻り値

  • uint256
    • 作成された転送バリデーションの合計数。

totalApprovalValidations

javascript
function totalApprovalValidations() external view returns (uint256);

概要 作成された承認バリデーションの合計数を返す関数。

詳細 totalApprovalValidations 関数は、これまでに作成されたすべての承認バリデーションの数を返すために使用されます。

戻り値

  • uint256
    • 作成された承認バリデーションの合計数。

isValidatorContract() 関数は、コントラクトがバリデータであるかどうかを示すため、publicとして実装する必要があります。 これにより、外部のエンティティや他のコントラクトからこの関数を呼び出すことが可能になります。

transferValidation(uint256 transferId) と approvalValidation(uint256 approveId) 関数は、publicまたはexternalとして実装できます。 publicはコントラクト内外からアクセス可能ですが、externalはコントラクトの外部からのみアクセス可能です。 これらの関数は、特定の転送または承認の検証情報を取得するために使用されます。

totalTransferValidations() と totalApprovalValidations() 関数は、pureまたはviewとして実装できます。 pure関数は、コントラクトの状態を読み取らず、変更もしません。 view関数はコントラクトの状態を読み取ることができますが、変更はできません。 これらの関数は、それぞれ転送検証と承認検証の総数を返します。

補足

汎用性

この標準は、バリデーション機能は定義されていますが、それらの使用方法についての具体的な指示はありません。 バリデーションは内部的に定義されており、ユーザーは自身でそれらを管理する方法を決定できます。 例えば、dAppに接続されたアドレスバリデータを使用して、ユーザーが自分のバリデーションを管理する場合が考えられます。 このバリデータは、すべてのNFTに対して、または特定のユーザーに対してのみ使用される可能性があります。 さらに、既存のERC721コントラクトをラップする形で使用され、既存のNFTとの1対1の変換を可能にすることもできます。

拡張性

この標準がバリデーション関数を定義しているものの、それを検証するシステムについては定義していません。 サードパーティのプロトコルは、これらの関数を任意の方法で呼び出す方法を定義できます。 これにより、様々な実装や応用が可能になり、基準の柔軟性が高まります。

互換性

この基準はERC721の拡張版であり、transferFrom、safeTransferFrom、approve、setApprovalForAllという操作を除いて、ERC721のすべての操作と互換性があります。 これらの特定の操作については、NFTの所有権を移転するか承認を有効にするのではなく、バリデーションの請願を作成するように機能が上書きされます。

実装

javascript
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "./IERC6997.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

/**
 * @dev Implementation of ERC6997
 */
contract ERC6997 is IERC6997, ERC721 {

    // Mapping from transfer ID to transfer validation
    mapping(uint256 => TransferValidation) private _transferValidations;

    // Mapping from approval ID to approval validation
    mapping(uint256 => ApprovalValidation) private _approvalValidations;

    // Total number of transfer validations
    uint256 private _totalTransferValidations;

    // Total number of approval validations
    uint256 private _totalApprovalValidations;

    /**
     * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
     */
    constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){
    }

    /**
    * @dev Returns true if this contract is a validator ERC721.
    */
    function isValidatorContract() public pure returns (bool) {
        return true;
    }

    /**
     * @dev Returns the transfer validation struct using the transfer ID.
     *
     */
    function transferValidation(uint256 transferId) public view override returns (TransferValidation memory) {
        require(transferId < _totalTransferValidations, "ERC6997: invalid transfer ID");
        TransferValidation memory v = _transferValidation(transferId);

        return v;
    }

    /**
     * @dev Returns the approval validation struct using the approval ID.
     *
     */
    function approvalValidation(uint256 approvalId) public view override returns (ApprovalValidation memory) {
        require(approvalId < _totalApprovalValidations, "ERC6997: invalid approval ID");
        ApprovalValidation memory v = _approvalValidation(approvalId);

        return v;
    }

    /**
     * @dev Return the total amount of transfer validations created.
     *
     */
    function totalTransferValidations() public view override returns (uint256) {
        return _totalTransferValidations;
    }

    /**
     * @dev Return the total amount of approval validations created.
     *
     */
    function totalApprovalValidations() public view override returns (uint256) {
        return _totalApprovalValidations;
    }

    /**
     * @dev Returns the transfer validation of the `transferId`. Does NOT revert if transfer doesn't exist
     */
    function _transferValidation(uint256 transferId) internal view virtual returns (TransferValidation memory) {
        return _transferValidations[transferId];
    }

    /**
     * @dev Returns the approval validation of the `approvalId`. Does NOT revert if transfer doesn't exist
     */
    function _approvalValidation(uint256 approvalId) internal view virtual returns (ApprovalValidation memory) {
        return _approvalValidations[approvalId];
    }

    /**
     * @dev Validate the transfer using the transfer ID.
     *
     */
    function _validateTransfer(uint256 transferId) internal virtual {
        TransferValidation memory v = transferValidation(transferId);
        require(!v.valid, "ERC6997: the transfer is already validated");

        address from = v.from;
        address to = v.to;
        uint256 tokenId = v.tokenId;

        super._transfer(from, to, tokenId);

        _transferValidations[transferId].valid = true;
    }

    /**
     * @dev Validate the approval using the approval ID.
     *
     */
    function _validateApproval(uint256 approvalId) internal virtual {
        ApprovalValidation memory v = approvalValidation(approvalId);
        require(!v.valid, "ERC6997: the approval is already validated");

        if(!v.approveAll) {
            require(v.owner == ownerOf(v.tokenId), "ERC6997: The token have a new owner");
            super._approve(v.approve, v.tokenId);
        }
        else {
            super._setApprovalForAll(v.owner, v.approve, true);
        }

        _approvalValidations[approvalId].valid = true;
    }

    /**
     * @dev Create a transfer petition of `tokenId` from `from` to `to`.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     *
     * Emits a {TransferValidate} event.
     */
    function _transfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override {
        require(ERC721.ownerOf(tokenId) == from, "ERC6997: transfer from incorrect owner");
        require(to != address(0), "ERC6997: transfer to the zero address");

        if(_msgSender() == from) {
            TransferValidation memory v;

            v.from = from;
            v.to = to;
            v.tokenId = tokenId;

            _transferValidations[_totalTransferValidations] = v;

            emit ValidateTransfer(from, to, tokenId, _totalTransferValidations);

            _totalTransferValidations++;
        } else {
            super._transfer(from, to, tokenId);
        }
    }

    /**
     * @dev Create an approval petition from `to` to operate on `tokenId`
     *
     * Emits an {ValidateApproval} event.
     */
    function _approve(address to, uint256 tokenId) internal override virtual {
        ApprovalValidation memory v;

        v.owner = ownerOf(tokenId);
        v.approve = to;
        v.tokenId = tokenId;

        _approvalValidations[_totalApprovalValidations] = v;

        emit ValidateApproval(v.owner, to, tokenId, false, _totalApprovalValidations);

        _totalApprovalValidations++;
    }

    /**
     * @dev If approved is true create an approval petition from `operator` to operate on
     * all of `owner` tokens, if not remove `operator` from operate on all of `owner` tokens
     *
     * Emits an {ValidateApproval} event.
     */
    function _setApprovalForAll(
        address owner,
        address operator,
        bool approved
    ) internal override virtual {
        require(owner != operator, "ERC6997: approve to caller");

        if(approved) {
            ApprovalValidation memory v;

            v.owner = owner;
            v.approve = operator;
            v.approveAll = true;

            _approvalValidations[_totalApprovalValidations] = v;

            emit ValidateApproval(v.owner, operator, 0, true, _totalApprovalValidations);

            _totalApprovalValidations++;
        }
        else {
            super._setApprovalForAll(owner, operator, approved);
        }
    }
}

セキュリティ

仕様に定義されているように、NFTの所有権を変更する操作やNFTの管理権を承認する操作は、TransferValidation または ApprovalValidation を保留状態で作成し、NFTの所有権を移転するか承認を有効にしてはなりません。

この基準に基づき、TransferValidation や ApprovalValidation の検証は、適用されるシステムに必要とされる最大限のセキュリティで保護される必要があります。 例として、トランザクションを検証するためのバリデータアドレスを持つシステムがあります。 また、各ユーザーが自分のバリデータアドレスを選択できるシステムも有効な方法です。

どちらの場合も、選択されたシステムの許可なしに、どのアドレスもTransferValidation や ApprovalValidation を検証できません。 これにより、NFTの所有権の移転や承認プロセスのセキュリティが強化されることになります。

引用

Eduard López i Fina (@eduardfina), "ERC-6997: ERC-721 with transaction validation step. [DRAFT]," Ethereum Improvement Proposals, no. 6997, May 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6997.

最後に

今回は「NFTのtransferとapproveに新たな認証ステップを追加し、ウォレット内からの不正なNFTの流出を防ぐ仕組みを提案している規格であるERC6997」についてまとめてきました! いかがだったでしょうか?

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

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