LoginSignup
6
3

[ERC7144] ERC20トークンのapproveとtransferの安全性チェックの仕組みを理解しよう!

Posted at

はじめに

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

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

今回は、ERC20トークンの承認や送付が安全であるかをチェックする仕組みを提案している規格であるERC7144についてまとめていきます!

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

7144は現在(2023年12月15日)では「Review」段階です。

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

概要

この新しい標準は、ERC20をもっと安全にするためのアップグレードした規格です。
ERC20は、ブロックチェーン上でトークンのやり取りをする基本的な方法です。
この新しいアップグレードでは、トークンを送ったり承認したりする時に、その操作が安全であるかどうかをチェックするために一時的にロックする機能が加わります。

例えば、あなたがトークンを誰かに送るとき、その送信は即座には行われません。
まず、その送信が安全かどうかを確認するためのステップがあります。
これにより、不正な取引やウォレットからのお金の盗難を防ぐことができます。

このセキュリティのアップグレードは、スマートコントラクトを通じて行われます。
スマートコントラクトは、ブロックチェーン上で動作するプログラムのようなもので、この新しい機能によってトークンの持ち主はより安全に自分の資産を管理できるようになります。

この規格は具体的な検証の仕組みなどは提供せずに、承認の枠組みを提案している規格になります。

動機

ブロックチェーンは、ユーザーが自分のデータを完全にコントロールするという大きな利点を持っていますが、それは同時にリスクも伴います。
特に、トークン盗難は大きな問題です。
今ある盗難防止策、例えばトークンをオフラインの「コールドウォレット」に移す方法は、使い勝手を悪くしてしまいます。

コールドウォレット
コールドウォレットとは、インターネットから切り離された状態で仮想通貨やトークンなどのデジタル資産を保管する方法です。
これは、ホットウォレットと対照的な存在で、ホットウォレットはインターネットに接続されているため、取引が容易ですが、セキュリティの面でリスクが伴います。

コールドウォレットの主な特徴は以下の通りです。

  • オフライン保管
    • コールドウォレットはインターネットに接続されていないため、ハッキングやオンライン上の脅威から保護されています。
  • セキュリティの強化
    • オフライン状態であることから、サイバー攻撃やマルウェアのリスクが大幅に低下します。
  • 利用の手間
    • ホットウォレットと比べて取引に時間がかかり、少し手間がかかることがあります。
    • コールドウォレットから資産を動かすためには、物理的にウォレットをオンライン環境に接続する必要があるからです。
  • 種類
    • コールドウォレットにはいくつかの形態があります。
    • 例えば、ハードウェアウォレット(特殊なUSBデバイス)、ペーパーウォレット(秘密鍵を紙に印刷)、または単純にコンピュータやスマートフォンをインターネットから完全に切り離したものなどです。
  • 長期保管に適している
    • コールドウォレットは、頻繁にアクセスする必要がない資産の長期保管に特に適しています。

総合すると、コールドウォレットはセキュリティが高い代わりに、取引の際には少し手間がかかる保管方法です。
特に大量の資産を保有している場合や、長期間保管する目的で利用されることが多いです。

ここで、新しい解決策が提案されています。
それは、トークンを送る(transfer)時や承認(approve)する時に、その都度バリデーション(検証)ステップを挟むことです。
これにより、スマートコントラクトの開発者は、より安全な盗難防止策を考えることができます。

例えば、1つの実装方法として、特定のバリデータアドレスが全てのスマートコントラクトのトランザクションを検証するシステムがあります。
このアドレスはdApp(分散型アプリケーション)に接続されており、ユーザーはこのdAppを通じて自分のトークンに関するバリデーションリクエストを確認し、正しいものだけを承認することができます。

この方法では、バリデータアドレスにはトランザクションの検証だけの権限が与えられます。
そのため、トークンを盗もうとする人は、ユーザーのアドレスとバリデータアドレスの両方を同時にコントロールする必要があり、これが難しいため、システム全体の安全性が向上します。

仕様

この**EIP(Ethereum Improvement Proposal)**は、ERC20に準拠したコントラクトに新しい機能を追加することを提案しています。

まず、トークンの所有権を変更する操作、たとえばtransfertransferFromを行う時、すぐにトークンを移動させるのではなく、「TransferValidation(転送検証)」という待機状態を作成し、「ValidateTransfer(転送検証イベント)」を発行する必要があります。
この時点ではトークンは移動されません。

次に、トークンの管理を許可する操作、例えばapproveを行う時も、「ApprovalValidation(承認検証)」という待機状態を作成し、「ValidateApproval(承認検証イベント)」を発行する必要があります。
この時点ではまだ承認は有効にされません。

ただし、トークンの所有者ではなく承認されたアカウントからtransferが呼び出される場合は、バリデーションなしで直接実行されます。
これは、トークンを直接移動するために承認されたアカウントが必要な現行のプロジェクトに対応するためです。

TransferValidationまたはApprovalValidationの検証を行う際には、validフィールドをtrueに設定し、それ以上の検証は行わない必要があります。

TransferValidationを検証する操作は、トークンの所有権を変更するロールを持ちます。
一方、ApprovalValidationを検証する操作は、承認を有効にするロールを持ちます。

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

interface IERC7144 {

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

    struct ApprovalValidation {
        // The address of the owner.
        address owner;
        // The spender address.
        address spender;
        // The token amount approved.
        uint256 amount;
        // Whether is a valid approval.
        bool valid;
    }

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

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

    /**
     * @dev Returns true if this contract is a validator ERC20.
     */
    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);
}

isValidatorContract()関数

この関数は、コントラクトがトランザクションの検証を行う「バリデータコントラクト」かどうかを判断します。
これは公開されている関数で、誰でもこのコントラクトがバリデータかどうかを確認できます。

transferValidation(uint256 transferId)関数

この関数は、特定の転送IDに関連する転送検証の詳細を取得します。
この関数は公開されるか、または外部からのみアクセス可能な関数として実装されるかもしれません。
つまり、他のコントラクトやユーザーがこの情報を取得できます。

approvalValidation(uint256 approveId)関数

この関数は、特定の承認IDに関連する承認検証の詳細を取得します。
これもtransferValidationと同じように、公開されるか外部からのみアクセス可能な関数として実装される可能性があります。

totalTransferValidations()関数

これは、これまでに作成された転送検証の総数を返す関数です。
この関数は「pure」(コントラクトの状態に影響を与えずに計算を行う)か「view」(コントラクトの状態を読み取るが変更しない)として実装されるかもしれません。

totalApprovalValidations()関数**:これは、これまでに作成された承認検証の総数を返す関数です。これもtotalTransferValidationsと同様に、「pure」か「view」として実装される可能性があります。

これらの関数は、スマートコントラクトでのトランザクションや承認のプロセスを管理し、追跡するために重要です。

変数

TransferValidation

struct TransferValidation {
    address from;
    address to;
    uint256 amount;
    bool valid;
}

概要

トークン転送を検証する構造体。

詳細

TransferValidationはトークン転送の詳細情報を格納します。
これには送信者アドレス、受信者アドレス、転送するトークン量、そしてその転送が有効であるかどうかが含まれます。

パラメータ

  • from
    • トークンを送るユーザーのアドレス。
  • to
    • トークンを受け取るユーザーのアドレス。
  • amount
    • 転送されるトークンの量。
  • valid
    • この転送が有効であるかどうかのboolean値。

ApprovalValidation

struct ApprovalValidation {
    address owner;
    address spender;
    uint256 amount;
    bool valid;
}

概要

トークン承認を検証する構造体。

詳細

ApprovalValidationはトークン承認の詳細情報を格納します。
これにはトークンの所有者アドレス、使用許可を受けるユーザーのアドレス、承認されるトークン量、そしてその承認が有効であるかどうかが含まれます。

パラメータ

  • owner
    • トークンの所有者のアドレス。
  • spender
    • トークンを使用する許可を受けるユーザーのアドレス。
  • amount
    • 承認されるトークンの量。
  • valid
    • この承認が有効であるかどうかのboolean値。

ValidateTransfer

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

概要

新しい転送検証が要求されたときに発行されるイベント。

詳細

ValidateTransferイベントは、トークンの転送に関する検証リクエストが作成されたときに発行されます。
このイベントは転送の詳細情報と共に転送検証IDも提供します。

パラメータ

  • from
    • トークンを送るユーザーのアドレス。
  • to
    • トークンを受け取るユーザーのアドレス。
  • amount
    • 転送されるトークンの量。
  • transferValidationId
    • 転送検証の識別ID。

ValidateApproval

event ValidateApproval(address indexed owner, address indexed spender, uint256 amount, uint256 indexed approvalValidationId);

概要

新しい承認検証が要求されたときに発行されるイベント。

詳細

ValidateApprovalイベントは、トークンの承認に関する検証リクエストが作成されたときに発行されます。
このイベントは承認の詳細情報と共に承認検証IDも提供します。

パラメータ

  • owner
    • トークンの所有者のアドレス。
  • spender
    • トークンを使用する許可を受けるユーザーのアドレス。
  • amount
    • 承認されるトークンの量。
  • approvalValidationId
    • 承認検証の識別ID。

関数

isValidatorContract

function isValidatorContract() external view returns (bool);

概要

このコントラクトがERC20のバリデータであるか判断する関数。

詳細

isValidatorContract関数は、コントラクトがERC20トークンのバリデーション(検証)を行うためのバリデータであるかを確認するために使用されます。

戻り値

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

transferValidation

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

概要

指定された転送IDに基づく転送検証データを取得する関数。

詳細

transferValidation関数は、特定の転送IDに関連付けられたTransferValidation構造体のインスタンスを返します。
これにより、特定の転送に関する詳細情報を取得できます。

引数

  • transferId
    • 転送検証を取得するための転送ID。

戻り値

  • TransferValidation
    • 指定された転送IDに関連するTransferValidation構造体。

approvalValidation

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

概要

指定された承認IDに基づく承認検証データを取得する関数。

詳細

approvalValidation関数は、特定の承認IDに関連付けられたApprovalValidation構造体のインスタンスを返します。
これにより、特定の承認に関する詳細情報を取得できます。

引数

  • approvalId
    • 承認検証を取得するための承認ID。

戻り値

  • ApprovalValidation
    • 指定された承認IDに関連するApprovalValidation構造体。

totalTransferValidations

function totalTransferValidations() external view returns (uint256);

概要

作成された転送検証の総数を返す関数。

詳細

totalTransferValidations関数は、これまでに作成された全ての転送検証の数をカウントして返します。

戻り値

  • uint256
    • 作成された転送検証の総数。

totalApprovalValidations

function totalApprovalValidations() external view returns (uint256);

概要

作成された承認検証の総数を返す関数。

詳細

totalApprovalValidations関数は、これまでに作成された全ての承認検証の数をカウントして返します。

戻り値

  • uint256
    • 作成された承認検証の総数。

補足

この標準規格は、トークンのバリデーション(検証)に関する基本的な枠組みを定めていますが、その具体的な使用方法については柔軟性を持たせています。
この点での「汎用性」と「拡張性」が重要な特徴です。

汎用

  • スタンダードではバリデーションの基本的な機能を定義していますが、それらをどう使うかは開発者やユーザーが決められます。
    • これは、バリデーション機能が「内部的な」ものとして設計されているためです。
  • 例えば、dApp(分散型アプリケーション)に接続された特定のアドレスバリデータを使って、ユーザーが自分のトークンのバリデーションを管理することができます。
  • このバリデータは、すべてのトークンに対して使うことも、特定のユーザーに限定して使うこともできます。
  • また、既存のERC20コントラクトをラップする形で使用し、既存のトークンと1対1で交換できるようにすることも可能です。

拡張性

  • スタンダードはバリデーション機能を定義していますが、その検証をどのようなシステムで行うかについては指定していません。
  • 第三者のプロトコルがこれらの機能を自由に呼び出し、利用することができます。

このスタンダードはトークンのバリデーションに関するルールを提供していますが、その適用方法は開発者やユーザーが自由にアレンジできるようになっています。
これにより、さまざまなニーズや状況に合わせた柔軟なバリデーションシステムを構築することが可能になります。

後方互換性

この説明は、ERC20トークンの拡張スタンダードについてのものです。この拡張では、ERC20の通常の操作に対応していますが、transfertransferFromapproveという特定の操作に関しては、新しい方法が採用されています。

普通のERC20コントラクトでは、これらの操作を使ってトークンを直接移動したり、承認したりします。しかし、この拡張スタンダードでは、これらの操作が直接トークンを移動するか、承認を有効にするのではなく、「バリデーション請求」を生成するように変更されています。これは、ユーザーがトークンの転送や何かの承認を試みる際、その操作がただちに実行されるのではなく、まずその操作が適切かどうかを判断するための検証プロセスが開始されることを意味します。

この方法により、トークンの転送や承認に追加のセキュリティ層が加えられ、不正な取引や誤った承認のリスクを減らすことができるようになります。これは、トークンの取引と管理をより安全にするための重要なステップです。

参考実装

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./IERC7144.sol";

/**
 * @dev Implementation of ERC7144
 */
contract ERC7144 is IERC7144, ERC20 {

    // 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_) ERC20(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, "ERC7144: 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, "ERC7144: 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, "ERC721V: the transfer is already validated");

        super._transfer(v.from, v.to, v.amount);

        _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, "ERC7144: the approval is already validated");

        super._approve(v.owner, v.spender, v.amount);

        _approvalValidations[approvalId].valid = true;
    }

    /**
     * @dev Create a transfer petition of `tokenId` from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     *
     * Emits a {ValidateTransfer} event.
     */
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
        require(from != address(0), "ERC7144: transfer from the zero address");
        require(to != address(0), "ERC7144: transfer to the zero address");

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

            v.from = from;
            v.to = to;
            v.amount = amount;

            _transferValidations[_totalTransferValidations] = v;

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

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

    /**
     * @dev Create an approval petition from `owner` to operate the `amount`
     *
     * Emits an {ValidateApproval} event.
     */
    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual override {
        require(owner != address(0), "ERC7144: approve from the zero address");
        require(spender != address(0), "ERC7144: approve to the zero address");

        ApprovalValidation memory v;

        v.owner = owner;
        v.spender = spender;
        v.amount = amount;

        _approvalValidations[_totalApprovalValidations] = v;

        emit ValidateApproval(v.owner, spender, amount, _totalApprovalValidations);

        _totalApprovalValidations++;
    }
}

セキュリティ考慮事項

この説明は、トークンの所有権を変更する操作やトークンの管理に関する承認を行う時の新しいスタンダードについてのものです。
このスタンダードでは、トークンの転送(transfer)や承認(approve)の操作が通常通り即座に実行されるのではなく、一旦「TransferValidation(転送検証)」や「ApprovalValidation(承認検証)」という保留状態にされます。
つまり、これらの操作は実際にトークンを移動したり、承認を有効にしたりする前に適切な検証が必要となります。

このシステムでは、転送検証や承認検証を行う操作には非常に高いセキュリティが求められます。
例えば、トランザクションを検証する特定のバリデータアドレスを設けることが考えられます。
ユーザーは自分のバリデータアドレスを選択することもでき、このアドレスだけがトランザクションの検証を行うことができます。

このスタンダードの重要な点は、選ばれたシステムの許可がなければどのアドレスも転送検証や承認検証を行うことができないということです。
これにより、トークンの取引や管理がより安全になり、不正な取引や誤った承認のリスクを低減できるというわけです。

引用

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

最後に

今回は「ERC20トークンの承認や送付が安全であるかをチェックする仕組みを提案している規格であるERC7144」についてまとめてきました!
いかがだったでしょうか?

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

Twitter @cardene777

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

6
3
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
6
3