はじめに

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

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

今回は、ERC3009を拡張してERC1271をサポートすることで、コントラクトでの署名にも対応できる仕組みを提案しているERC7598についてまとめていきます!

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

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

概要

この規格では、ERC3009の機能を拡張して、コントラクトウォレットでのapprove + transferFromのサポートを提案しています。

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

動機

ERC3009では、EOAアドレスによるECDSA署名をもとに別アドレスのトークンの送付が可能でした。 ただし、コントラクトウォレットの普及するにつれて既存の標準(ERC3009)では十分ではなくなってきます。 この提案では、ERC1271で定義されているように、ERC3009をコントラクトウォレットによる署名の検証機能を追加することで、使いやすさとコンポーザビリティを高めることを目的としています。 この拡張機能を実装することで、ユーザーは安全なapproveプロセスを実行しながら資産管理の柔軟性を高めることができます。

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

仕様

ERC3009で定義されている、以下のイベントとインターフェースは実装する必要があります。

  • AuthorizationUsedイベント。
  • TRANSFER_WITH_AUTHORIZATION_TYPEHASHとRECEIVE_WITH_AUTHORIZATION_TYPEHASH定数。
  • authorizationState(address authorizer, bytes32 nonce)関数。

また、ここで提案されている標準に準拠するには、以下のインターフェースを追加する必要があります。

js
/**
 * @notice Execute a transfer with a signed authorization
 * @param from          Payer's address (Authorizer)
 * @param to            Payee's address
 * @param value         Amount to be transferred
 * @param validAfter    The time after which this is valid (unix time)
 * @param validBefore   The time before which this is valid (unix time)
 * @param nonce         Unique nonce
 * @param signature     Unstructured bytes signature signed by an EOA wallet or a contract wallet
 */
function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes memory signature
) external;

/**
 * @notice Receive a transfer with a signed authorization from the payer
 * @dev This has an additional check to ensure that the payee's address matches
 * the caller of this function to prevent front-running attacks. (See security
 * considerations)
 * @param from          Payer's address (Authorizer)
 * @param to            Payee's address
 * @param value         Amount to be transferred
 * @param validAfter    The time after which this is valid (unix time)
 * @param validBefore   The time before which this is valid (unix time)
 * @param nonce         Unique nonce
 * @param signature     Unstructured bytes signature signed by an EOA wallet or a contract wallet
 */
function receiveWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes memory signature
) external;

transferWithAuthorization

署名付きの認証を使ってトークンのtransferを実行する関数。

  • from
    • トークンを支払うアドレス(認証者)。
  • to
    • トークンを受け取るアドレス。
  • value
    • transferされるトークンの量。
  • validAfter
    • この時間(Unixタイム)以降に有効となる時間。
  • validBefore
    • この時間(Unixタイム)までに有効な時間。
  • nonce
    • ユニークな値(再利用を防ぐための一意の値)。
  • signature
    • EOAウォレットまたはコントラクトウォレットによって署名された未構造化バイトの署名。

未構造化バイト入力とは、特定の形式や構造に従わないバイト列を指します。 この入力形式は、データを自由な形式で表現できるため柔軟性があります。

背景

従来の署名検証スキームでは、通常、署名は v, r, s という3つのパラメータに分かれていました。 これらは、特定のECDSA(Elliptic Curve Digital Signature Algorithm)署名の形式に基づいています。

未構造化バイト入力のメリット

未構造化バイト入力では、署名を特定の構造に分割するのではなく、1つの連続したバイト列として扱います。 これにより、以下のようなメリットがあります。

  • 柔軟性の向上
    • 様々な署名スキームやアルゴリズムに対応可能です。
    • 特定の署名形式に縛られないため、新しい署名方式の導入が容易になります。
  • 簡素化
    • 署名データを1つのバイト列として扱うことで、署名の取り扱いが簡素化されます。
実装例
従来の署名形式

従来のV, R, S形式の署名検証は次のように行います。

js
function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    // 署名検証ロジック
}
未構造化バイト入力

未構造化バイト入力を使用する場合、署名データ全体を1つのバイト配列として渡します。

js
function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes memory signature
) external {
    // 署名検証ロジック
}

ここで、signature は、従来の v, r, s を含むバイト配列です。 例えば、abi.encodePacked(r, s, v) のようにエンコードされたバイト配列を渡します。

receiveWithAuthorization

支払者からの署名付き認証を使用してトークンの受け取りを実行する関数。

  • from
    • トークンを支払うアドレス(認証者)。
  • to
    • トークンを受け取るアドレス。
  • value
    • transferされるトークンの量。
  • validAfter
    • この時間(Unixタイム)以降に有効となる時間。
  • validBefore
    • この時間(Unixタイム)までに有効な時間。
  • nonce
    • ユニークな値(再利用を防ぐための一意の値)。
  • signature
    • EOAウォレットまたはコントラクトウォレットによって署名された未構造化バイトの署名。

オプションとして以下の機能を実装することができます。

js
/**
 * @notice Attempt to cancel an authorization
 * @param authorizer    Authorizer's address
 * @param nonce         Nonce of the authorization
 * @param signature     Unstructured bytes signature signed by an EOA wallet or a contract wallet
 */
function cancelAuthorization(
    address authorizer,
    bytes32 nonce,
    bytes memory signature
) external;

既存の署名付き認証をキャンセルする関数。 これにより、指定された認証が将来使用されるのを防ぐことができます。

  • authorizer
    • 認証を行ったアドレス(認証者)。
  • nonce
    • キャンセルする認証のナンス(一意の値)。
  • signature
    • EOAウォレットまたはコントラクトウォレットによって署名されたバイトの署名。

関連する以下の機能も使用されます。 AuthorizationCanceledイベントは、認証がキャンセルされたときに発生します。 CANCEL_AUTHORIZATION_TYPEHASH 定数は、キャンセル署名の型ハッシュを定義します。

補足

この変更の目的は、現在のV, R, S署名検証スキームを置き換え、未構造化バイト入力をサポートすることで、コントラクト開発者がEOA(Externally Owned Account)とSCウォレット(スマートコントラクトウォレット)の両方からの署名を検証するための統一されたインターフェースを使用できるようにすることです。 これにより、ウォレットの種類に応じた異なる署名スキームやアルゴリズムを利用することが可能となり、コントラクトウォレットや高度なウォレットタイプがその署名検証プロセスに対応できます。

互換性

この提案では、ERC3009との互換性があり、現在のV, R, S署名検証スキームに依存しているコントラクトは引き続き問題なく機能します。

互換性の確保

既存のV, R, S署名検証スキームと新しい未構造化バイト署名検証の両方をサポートする必要がある場合、開発者は以下のようなコードブロックを適用して重複を減らすことができます。 この方法では、V, R, S形式の署名を未構造化バイト形式に変換してから既存の関数に渡せます。

js
function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    transferWithAuthorization(
        from,
        to,
        value,
        validAfter,
        validBefore,
        nonce,
        abi.encodePacked(r, s, v)
    );
}

実装

js
/**
  * @notice Execute a transfer with a signed authorization
  * @dev EOA wallet signatures should be packed in the order of r, s, v.
  * @param from          Payer's address (Authorizer)
  * @param to            Payee's address
  * @param value         Amount to be transferred
  * @param validAfter    The time after which this is valid (unix time)
  * @param validBefore   The time before which this is valid (unix time)
  * @param nonce         Unique nonce
  * @param signature     Signature byte array produced by an EOA wallet or a contract wallet
  */
function _transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes memory signature
) internal {
    require(now > validAfter, "Authorization is not yet valid");
    require(now < validBefore, "Authorization is expired");
    require(!_authorizationStates[authorizer][nonce], "Authorization is used or canceled");

    bytes32 digest = keccak256(abi.encodePacked(
        hex"1901",
        DOMAIN_SEPARATOR,
        keccak256(abi.encode(
            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce
        ))
    ));
    require(
        // Check for both ECDSA signature and and ERC-1271 signature. A sample SignatureChecker is available at
        // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7bd2b2a/contracts/utils/cryptography/SignatureChecker.sol
        SignatureChecker.isValidSignatureNow(
            owner,
            typedDataHash,
            signature
        ),
        "Invalid signature"
    );

    _authorizationStates[authorizer][nonce] = true;
    emit AuthorizationUsed(authorizer, nonce);
    
    _transfer(from, to, value);
}

セキュリティ

コントラクトウォレットの署名検証

transferWithAuthorization、receiveWithAuthorization、cancelAuthorization の各関数のセキュリティは、ContractWallet.isValidSignature() 関数に依存しています。 この関数は、署名バイトがコントラクトウォレット所有者によって実行されたことを確認するために使用されます。

  • isValidSignature

    • 提供された署名が有効であり、適切な権限を持つコントラクトウォレット所有者によって作成されたものであることを確認する関数。
  • transferWithAuthorization

    • 認証者が署名したtransfer指示に基づいてトークンをtransferする関数。
    • 提供された署名が ContractWallet.isValidSignature() で有効と確認されることで、署名の正当性が保証されます。
  • receiveWithAuthorization

    • 支払者が署名したtransfer指示に基づいてトークンを受け取ります。
    • to アドレスが関数を呼び出したアドレスと一致することを確認する追加のチェックに加え、署名の正当性が ContractWallet.isValidSignature() によって保証されます。
  • cancelAuthorization

    • 既存の認証をキャンセルする関数。
    • キャンセル指示の署名が ContractWallet.isValidSignature() で有効と確認されることで、署名の正当性が保証されます。

具体例

以下は、カスタム署名検証ロジックを実装する際の注意点を示した例です。

solidity
contract ContractWallet {
    function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bool) {
        // カスタム署名検証ロジック
        // 例:ECDSAで署名を検証する
        address signer = ECDSA.recover(hash, signature);
        return signer == owner;
    }
}

まとめ

  • isValidSignature 関数の重要性: 署名が適切に検証されることで、セキュリティが確保されます。
  • 開発者への注意喚起: コントラクトウォレット開発者は、カスタム署名検証ロジックを実装する際に慎重になる必要があります。不適切な実装は、意図しないトランザクションの実行などのセキュリティリスクを引き起こす可能性があります。

コントラクトウォレットの署名検証ロジックが適切に実装されていることを確認することで、これらの関数のセキュリティと信頼性が保たれます。

引用

Yvonne Zhang (@yvonnezhangc), Aloysius Chan (@circle-aloychan), "ERC-7598: Use contract signature for signed transfer [DRAFT]," Ethereum Improvement Proposals, no. 7598, January 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7598.

最後に

今回は「ERC3009を拡張してERC1271をサポートすることで、コントラクトでの署名にも対応できる仕組みを提案しているERC7598」についてまとめてきました! いかがだったでしょうか?

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

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