はじめに

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

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

今回は、ERC2612を拡張してERC1271ベースの署名検証をサポートする仕組みを提案しているERC7597についてまとめていきます!

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

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

概要

この規格はERC2612という既存の企画を拡張して、ERC20トークンをガスレスで送付することをコントラクトウォレットから実行する仕組みを提案しています。

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

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

動機

現在、ERC2612ではv、r、sのパラメータを使用した署名スキームを使用して、署名の作成・検証はEOAウォレットに制限しています。 ただ、今後コントラクトウォレットが普及してERC1271の採用が増加してきた時、より柔軟な署名検証とコントラクトの署名検証ロジックを実装する必要があります。 構造化されていない署名バイトを入力することで、カスタムアルゴリズムと署名スキームを利用でき、より幅広くコントラクトウォレットに対応することができます。

構造化されていない署名バイト(unstructured signature bytes)とは、特定の形式や構造に縛られず任意のバイト列として扱われる署名のことを指します。 これにより、署名の検証方法や署名形式が固定されず柔軟にカスタムの署名アルゴリズムや署名スキームを利用することができます。

仕様

この規格に準拠するコントラクトは以下のpermtを実装する必要があります。

js
function permit(address owner, address spender, uint value, uint deadline, bytes memory signature) external
  • owner
    • トークンを許可するアカウントのアドレス
  • spender
    • 許可を受けるアカウントのアドレス
  • value
    • 許可するトークンの量
  • deadline
    • 署名の有効期限(タイムスタンプ)
  • signature
    • 署名データ

また、ERC2612で提案されている以下の機能の実装も必要です。

js
function nonces(address owner) external view returns (uint)
function DOMAIN_SEPARATOR() external view returns (bytes32)
  • nonces
    • 各ownerアドレスに対する現在のnonce値を返す
  • DOMAIN_SEPARATOR
    • EIP712に基づくドメインセパレータを返す

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

permit関数が正常に動作するためには、以下の条件が満たされている必要があります。

  • 現在のブロックタイムがdeadline以下であること

    • 現在のブロックタイムがdeadlineを超えている場合、トランザクションは無効です。
  • ownerがゼロアドレスでないこと

    • ownerがゼロアドレス(0x0)の場合、トランザクションは無効です。
  • nonces[owner]が現在のnonceと一致していること

    • permit関数呼び出し前のnonces[owner]の値が指定されたnonce値と一致している必要があります。
    • nonceとはこれまで実行してきたトランザクションの数のことで、ユニーク値です。
  • 署名の検証

    • ownerがEOA(Externally Owned Account)の場合、署名はabi.encodePacked(r, s, v)形式の有効なsecp256k1署名であること。
    • ownerがスマートコントラクトの場合、ownerコントラクトのisValidSignature()関数を使用して署名を検証します。

これらの条件が満たされない場合、permit関数はリバート(revert)します。

補足

既存のv、r、sの署名検証スキームを飛行豪華バイトに置き換えることで、コントラクト開発者は統合インタフェースを使用してEOAとSCウォレットの両方からの署名を検証できます。 これにより、ウォレットタイプに適合するさまざまな署名スキームとアルゴリズムを使用でき、コントラクトウォレットとMetamaskuなどのウォレットでの署名検証プロセスを強化することができます。

互換性

この提案は既存のERC2612標準と互換性があります。 現在、v, r, s署名検証スキームに依存しているコントラクトも引き続き動作します。

もし、v, r, s署名検証と新しい構造化されていないバイト署名検証の両方を互換性の理由でサポートする必要がある場合、重複を減らすために以下のコードを使用することができます。

js
function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v, 
    bytes32 r, 
    bytes32 s
) external {
    _permit(owner, spender, value, deadline, abi.encodePacked(r, s, v));
}

このコードでは、新しい構造化されていないバイト署名検証スキームをサポートするために、既存のv, r, s署名検証スキームを内部的に再利用しています。

  1. v, r, s署名検証

    • permit関数は、従来のV, R, Sパラメータ(uint8 v, bytes32 r, bytes32 s)を受け取ります。
    • これらのパラメータを、新しい構造化されていないバイト署名形式に変換します。
    • 具体的には、abi.encodePacked(r, s, v)を使用して、r, s, vを連結し、バイト列に変換します。
  2. 内部関数への委譲

    • 変換されたバイト列を内部関数_permitに渡します。
    • この内部関数は、新しい署名検証ロジックを実装していると想定されています。
    • これにより、既存の署名検証ロジックを変更せずに、新しい構造化されていないバイト署名検証スキームをサポートできます。

内部関数 _permit

以下は、内部関数_permitの実装例です。 この関数は、構造化されていないバイト署名を処理します。

js
function _permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    bytes memory signature
) internal {
    require(block.timestamp <= deadline, "Permit: expired deadline");
    require(owner != address(0), "Permit: invalid owner");

    uint nonce = nonces[owner];
    bytes32 structHash = keccak256(
        abi.encode(
            keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
            owner,
            spender,
            value,
            nonce,
            deadline
        )
    );

    bytes32 hash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
    address signer = recoverSigner(hash, signature);

    if (isContract(owner)) {
        require(IERC1271(owner).isValidSignature(hash, signature) == 0x1626ba7e, "Permit: invalid signature from contract");
    } else {
        require(signer == owner, "Permit: invalid signature");
    }

    nonces[owner] = nonce + 1;
    allowance[owner][spender] = value;
    emit Approval(owner, spender, value);
}

function isContract(address account) internal view returns (bool) {
    uint32 size;
    assembly {
        size := extcodesize(account)
    }
    return size > 0;
}

function recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address) {
    bytes32 r;
    bytes32 s;
    uint8 v;

    if (signature.length != 65) {
        return address(0);
    }

    assembly {
        r := mload(add(signature, 0x20))
        s := mload(add(signature, 0x40))
        v := byte(0, mload(add(signature, 0x60)))
    }

    if (v < 27) {
        v += 27;
    }

    return ecrecover(hash, v, r, s);
}
  • _permit関数は、署名を検証し、条件が満たされた場合にallowanceを設定し、noncesをインクリメントします。
  • 署名がEOAによるものかスマートコントラクトによるものかを判定し、それぞれに応じた署名検証を行います。

実装

js
/**
 * @notice Update allowance with a signed permit
 * @dev Signature bytes can be used for both EOA wallets and contract wallets.
 * @param owner       Token owner's address (Authorizer)
 * @param spender     Spender's address
 * @param value       Amount of allowance
 * @param deadline    The time at which the signature expires (unix time)
 * @param signature   Unstructured bytes signature signed by an EOA wallet or a contract wallet
 */
function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    bytes memory signature
) external {
    require(deadline >= now, "Permit is expired");
    require(owner != address(0), "ERC20: approve from the zero address");
    require(spender != address(0), "ERC20: approve to the zero address");

    bytes32 digest = keccak256(abi.encodePacked(
        hex"1901",
        DOMAIN_SEPARATOR,
        keccak256(abi.encode(
            keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
            owner,
            spender,
            value,
            nonce,
            deadline
        ))
    ));
    
    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"
    );
    
    allowed[owner][spender] = value;
    emit Approval(owner, spender, value);
}

セキュリティ

permit機能のセキュリティは、コントラクトウォレットのisValidSignature()関数に依存しています。

正確な署名の検証

isValidSignature()関数は、署名が本当に所有者によって作成されたものであることを検証する必要があります。 署名の形式、ハッシュ対象のデータ、署名アルゴリズムなどが正しく実装されていることを確認します。

リプレイ攻撃の防止

各トランザクションにはユニークな識別子(nonceなど)を使用し、同じ署名が複数回使用されないようにします。 トランザクションの有効期限を設定し、古い署名が再利用されないようにします。

マルチシグ対応

複数の署名者がいる場合、全員の署名が揃っていることを確認するロジックを実装します。 各署名者の署名順序や必要な署名数を適切に管理します。

署名データの完全性

署名対象のデータ(メッセージハッシュ)が改ざんされていないことを保証します。 メッセージに必要な情報がすべて含まれていることを確認し、欠落がないようにします。

引用

Yvonne Zhang (@yvonnezhangc), Aloysius Chan (@circle-aloychan), "ERC-7597: Signature Validation Extension for Permit [DRAFT]," Ethereum Improvement Proposals, no. 7597, January 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7597.

最後に

今回は「ERC2612を拡張してERC1271ベースの署名検証をサポートする仕組みを提案しているERC7597」についてまとめてきました! いかがだったでしょうか?

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

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