はじめに

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

今回は、コントラクトレベルでのセキュリティレイヤーを導入し、再利用可能なバリデーションの仕組みを提案しているERC7746についてまとめていきます!

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

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

概要

ERC7746では、コントラクトにおいてセキュリティレイヤー(ILayer)という標準インターフェースの導入を提案しています。 このセキュリティレイヤーは、コントラクトの内部ロジックとは独立した形で動作するミドルウェアのような仕組みで、関数の実行前後にバリデーション(検証)を行うことができます。 この仕組みを導入することで、コントラクトごとにセキュリティ機能を実装するのではなく、汎用的で再利用可能なセキュリティレイヤーを適用できるようになります。

動機

現在のコントラクトのセキュリティ対策は、コントラクト内部にバリデーション処理を埋め込む形式がほとんどです。 ただ、この方法にはいくつかの問題があります。

まず、バリデーションロジックがコントラクトのロジックと密結合してしまうため、セキュリティ上の修正や機能の追加が難しくなります。 例えば、アクセス制御や入力の検証などを直接コントラクト内に実装すると、コードの可読性が低下して保守性が下がります。 また、全てのセキュリティ対策を各コントラクトに個別に実装する必要があるため、一貫性が保たれず、脆弱性が生じるリスクが高まります。

セキュリティレイヤー標準の導入による解決策

ERC7746では、セキュリティレイヤーをミドルウェアとして導入することで、コントラクトのセキュリティ管理をモジュール化して拡張性を持たせることを目指しています。 セキュリティレイヤーの導入によって、以下のようなメリットが得られます。

独立したセキュリティプロバイダーの実現

セキュリティチェックの実装を外部の専門的なセキュリティプロバイダーに託すことができるようになります。 例えば、特定のアクセス制御ポリシーを提供するプロバイダー、トランザクションの入力を検証するプロバイダー、出力の整合性を確認するプロバイダーなど、専門性に特化したセキュリティサービスを利用できます。 これにより、コントラクト開発者はコントラクトの本来の機能に集中でき、セキュリティに関しては専門家が提供するレイヤーを適用するだけで済むようになります。

セキュリティの組み合わせが可能になる

複数のセキュリティレイヤーを組み合わせることで、コントラクトごとに異なるセキュリティ要件を満たすカスタマイズが可能になります。 例えば、あるコントラクトではKYC(本人確認)レイヤーを適用し、別のコントラクトではAIを活用した異常検知レイヤーを適用する、といったように柔軟なセキュリティ設計が可能になります。 従来の一律のバリデーション方式に比べ、より適切なセキュリティ対策を実装できます。

コントラクトのアップグレードせずにセキュリティを更新できる

従来の方式では、ロジックを変更するためにコントラクトをデプロイし直す必要がありましたが、その手間やガス代が問題となっていました。 しかし、ERC7746ではセキュリティレイヤー標準を導入することで、コントラクト本体を変更せずにセキュリティ機能をアップグレードできるようになります。 これにより、長期間にわたって運用されるプロジェクトにおいて大きなメリットとなります。

幅広いバリデーションが可能になる

このセキュリティレイヤーでは、単なるアクセス制御だけでなく、入力データの正当性検証、出力の整合性チェック、異常検知など、多様なバリデーション処理を適用できます。 例えば、入力値が一定の範囲内にあるかどうかをチェックしたり、トランザクションの出力が期待される条件を満たしているかの検証などをすることで、より安全なコントラクトの実行ができます。

仕様

ILayerインターフェースを実装するコントラクトは2つの関数を実装する必要があります。

js
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.20;

interface ILayer {
    /// @notice Validates a function call before execution.
    /// @param configuration Layer-specific configuration data.
    /// @param selector The function selector being called.
    /// @param sender The address initiating the call.
    /// @param value The amount of ETH sent with the call (if any).
    /// @param data The calldata for the function call.
    /// @return beforeCallResult Arbitrary data to be passed to `afterCallValidation`.
    /// @dev MUST revert if validation fails.
    function beforeCall(
        bytes memory configuration,
        bytes4 selector,
        address sender,
        uint256 value,
        bytes memory data
    ) external returns (bytes memory);

    /// @notice Validates a function call after execution.
    /// @param configuration Layer-specific configuration data.
    /// @param selector The function selector being called.
    /// @param sender The address initiating the call.
    /// @param value The amount of ETH sent with the call (if any).
    /// @param data The calldata for the function call.
    /// @param beforeCallResult The data returned by `beforeCallValidation`.
    /// @dev MUST revert if validation fails.
    function afterCall(
        bytes memory configuration,
        bytes4 selector,
        address sender,
        uint256 value,
        bytes memory data,
        bytes memory beforeCallResult
    ) external;
}

beforeCall

js
function beforeCall(
    bytes memory configuration,
    bytes4 selector,
    address sender,
    uint256 value,
    bytes memory data
) external returns (bytes memory);

概要
関数の実行前にバリデーション(検証)を行う関数。

詳細
この関数は、対象となる関数が実行される前に呼び出され、適切なセキュリティチェックを適用します。
バリデーションに失敗した場合は処理がrevertされ、関数の実行がブロックされます。
成功した場合、afterCallに渡されるデータ(beforeCallResult)を返します。
複数のレイヤーが設定されている場合、定義された順番で実行されていずれかのレイヤーがrevertすると処理全体が中断されます。

引数

  • configuration
    • セキュリティレイヤーごとの設定データ。
  • selector
    • 実行される関数のセレクター(関数の識別子)。
  • sender
    • 関数を呼び出したアドレス。
  • value
    • 送信されたETHの量(送信されていない場合は 0)。
  • data
    • 関数呼び出し時のカロリデータ(引数を含む)。

戻り値

  • beforeCallResult
    • afterCall に渡す任意のデータ。バリデーション結果に応じたデータを含めることができる。

afterCall

js
function afterCall(
    bytes memory configuration,
    bytes4 selector,
    address sender,
    uint256 value,
    bytes memory data,
    bytes memory beforeCallResult
) external;

概要
関数の実行後にバリデーションを行う関数。

詳細
この関数は、対象の関数が実行された後に呼び出され、実行結果が期待される条件を満たしているかどうかを検証します。
beforeCallの結果(beforeCallResult)を利用して、入力と出力の整合性を検証する処理を実装することが可能です。

引数

  • configuration
    • セキュリティレイヤーごとの設定データ。
  • selector
    • 実行された関数のセレクター。
  • sender
    • 関数を呼び出したアドレス。
  • value
    • 送信されたETHの量(送信されていない場合は 0)。
  • data
    • 関数呼び出し時のカロリデータ(引数を含む)。
  • beforeCallResult
    • beforeCallで返されたデータ。

補足

柔軟性とカスタマイズ性

ERC7746では、layerConfigというパラメータを導入することで、セキュリティレイヤーごとの設定を柔軟に管理できるようにしています。 これにより、1つのセキュリティレイヤーを複数のコントラクトで利用できるようになり、それぞれのコントラクトが異なるセキュリティ要件を持っていたとしても適切に適用できるようになります。 例えば、あるコントラクトでは取引の頻度を制限するレートリミットを適用し、別のコントラクトでは特定のアドレスだけが操作できるようなアクセス制御を設定することが可能になります。

静的でない呼び出しのサポート

セキュリティレイヤーは、stateを保持できるようになっています。 つまり、単なる関数の入力チェックに限らず、過去の取引データを元にしたバリデーションや動的なアクセス制御が可能になります。 例えば、一定時間内に特定のアドレスから送信されたトランザクションの回数をカウントし、過剰な取引をブロックするといったレートリミットのような機能を実装できます。 これにより、より高度なセキュリティ要件を満たすことができます。

厳格なバリデーションによるセーフティメカニズム

セキュリティレイヤーは、バリデーションに失敗した場合に即座にrevertする仕組みを持っています。 これにより、悪意のある取引や不正なデータを含む呼び出しが実行される前にブロックされるため、コントラクトのセキュリティが向上します。 例えば、入力データが期待する範囲内にない場合や、送信者の権限が不足している場合、セキュリティレイヤーが検知して取引を無効化できます。 この厳格なバリデーションにより、コントラクトの脆弱性を悪用する攻撃のリスクを軽減できます。

ガスコストのトレードオフ

セキュリティレイヤーの導入により、追加のガスコストがかかります。 しかし、ガスコストの増加よりもコントラクトのセキュリティの向上によるメリットの方が大きいと考えられています。 特に、ブロックチェーン技術が発展し、L2レイヤーによるガスコスト削減が進んでいることを考慮すると、長期的に見てセキュリティレイヤーの導入は合理的な選択肢となります。

参考実装

以下に参考実装がまとめられています。

ILayerインターフェースの参考実装がリポジトリに提供されています。 ここでは、Protected.solというコントラクトがRateLimitLayer.solというセキュリティレイヤーを利用する形で実装されています。RateLimitLayerはILayerを実装し、送信者ごとに1ブロック内で10件までのトランザクションしか許可しない制約を設定しています。

RateLimitLayer.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity =0.8.20;
import "../ILayer.sol";

contract RateLimitLayer is ILayer {
    mapping(address => mapping(bytes4 => uint256)) usage;
    mapping(address => mapping(bytes4 => uint256)) usageUpdatedAtBlock;

    function beforeCallValidation(
        bytes memory,
        bytes4 messageSig,
        address,
        uint256,
        bytes memory
    ) public returns (bytes memory) {
        if (usageUpdatedAtBlock[msg.sender][messageSig] != block.number) {
            usage[msg.sender][messageSig] = 0;
            usageUpdatedAtBlock[msg.sender][messageSig] = block.number;
        } else {
            usage[msg.sender][messageSig] += 1;
        }
        return "";
    }

    function afterCallValidation(
        bytes memory layerConfig,
        bytes4 messageSig,
        address,
        uint256,
        bytes memory,
        bytes memory
    ) public view {
        uint256 blockQuota = uint256(bytes32(layerConfig));
        require(usage[msg.sender][messageSig] < blockQuota, "Rate limited");
    }
}

セキュリティ

セキュリティレイヤーを導入する時には、レイヤー自体の信頼性を確認することが必要です。 もしセキュリティレイヤーに悪意のあるコードが含まれていた場合、本来守るべきコントラクトが逆に攻撃者にとって都合の良い動作をするように制御されることになります。 そのため、セキュリティレイヤーを導入する時は、十分な監査を受けた信頼できる実装を選択する必要があります。

特に、中央集権的なセキュリティプロバイダーを利用する場合、そのプロバイダーの意図によっては意図的に制約を緩和したり、特定の条件下で処理を許可するなどのリスクが存在します。 そのため、オープンソースの監査済みのレイヤーを選択することや、分散型のセキュリティオラクルと連携することが推奨されます。

引用

Tim Pechersky (@peersky), "ERC-7746: Composable Security Middleware Hooks [DRAFT]," Ethereum Improvement Proposals, no. 7746, July 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7746.

最後に

今回は「コントラクトレベルでのセキュリティレイヤーを導入し、再利用可能なバリデーションの仕組みを提案しているERC7746」についてまとめてきました! いかがだったでしょうか?

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

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