banner
zach

zach

github
twitter
medium

ERC-2612: approveの拡張

ERC-2612 とは何か#

ERC-2612: EIP-20 署名承認のための Permit 拡張

ERC-2612 は erc20 の approve に対する最適化であり、従来の approve は EOA によって発起される必要があります。EOA にとって approve とその後の操作は少なくとも 2 つのトランザクションを必要とし、追加のガスコストが発生します。erc2612 は erc20 の拡張であり、approve を実現するための新しいメソッドを導入します。

erc-2612 は 3 つの新しいメソッドを実装する必要があります:

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
function nonces(address owner) external view returns (uint)
function DOMAIN_SEPARATOR() external view returns (bytes32)

image

重要なのは permit メソッドであり、このメソッドは erc20 の approval 構造を変更し、イベントを発火させます。

permit メソッドには 7 つのパラメータを渡す必要があります:

  • owner:現在のトークンのオーナー
  • spender:承認者
  • value:承認する額
  • deadline:トランザクション実行の締切
  • v:トランザクション署名データ
  • r:トランザクション署名データ
  • s:トランザクション署名データ
function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");

        // オーバーフローは発生しないため、未チェック
        unchecked {
            address recoveredAddress = ecrecover(
                keccak256(
                    abi.encodePacked(
                        "\x19\x01",
                        DOMAIN_SEPARATOR(),
                        keccak256(
                            abi.encode(
                                keccak256(
                                    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                                ),
                                owner,
                                spender,
                                value,
                                nonces[owner]++,
                                deadline
                            )
                        )
                    )
                ),
                v,
                r,
                s
            );

            require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");

            allowance[recoveredAddress][spender] = value;
        }

        emit Approval(owner, spender, value);
    }

ERC-2612 の使用フロー#

permit#

permit の主なフローは以下の通りです:

  • 現在のトランザクションの締切を検証
  • ecrecover を使用して recoveredAddress アドレスを解析
  • recoveredAddress が渡された owner アドレスと一致するかを検証
  • approve ロジックを実行し、allowance を変更し、Approval イベントを発火させる

重要なプロセスは 2 番目のステップであり、渡されたパラメータから recoveredAddress を解析し、渡された owner と一致するかを検証し、token.approve(spender, value)のようなロジックを実行して approve を完了します。

まず、内部のロジックをuncheckedでラップし、unchecked を使用する理由はいくつかあります:

  • Solidity 0.8.0 以降のオーバーフローに対するデフォルトのリバートと以前のコードの衝突を解決するため。自分の計算がオーバーフローしないことを確信できる場合、外側で unchecked でラップして Solidity にオーバーフローの判断をさせないことで、追加のガスコストを避けることができます。
  • 0.8.0 以降は SafeMath が不要になります。

次に、ecrecoverというキーワードが使われています。このメソッドの入力パラメータは:

  • bytes32:署名されたメッセージ
  • uint8: v
  • bytes32: r
  • bytes32: s

返されるパラメータは署名されたアドレス recoveredAddress です。

rvs はすべて渡されたパラメータであり、署名されたメッセージが何であるかを見てみましょう。

keccak256(
    abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR(),
        keccak256(
            abi.encode(
                keccak256(
                    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                ),
                owner,
                spender,
                value,
                nonces[owner]++,
                deadline
            )
        )
    )
)

keccak256は Solidity でハッシュを計算するための組み込みメソッドであり、abi.encodePackedは渡されたパラメータを密にエンコードします(0 のパディングを省略し、スペースを節約します)。

DOMAIN_SEPARATOR#

もう一つのメソッドはDOMAIN_SEPARATOR()であり、これは erc-2612 で実装する必要があるメソッドです。このフィールドの目的は、各チェーン上の各コントラクトにユニークな識別子を与え、EIP-712 の要件を満たすことです(標準的な構造化署名プロトコルであり、オフチェーン署名とオンチェーン検証の安全性を保証します。その中のドメインセパレーターはリプレイ攻撃を防ぐためのユニークな識別子です)。

EIP-712

function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
        return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
  }

function computeDomainSeparator() internal view virtual returns (bytes32) {
    return
        keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256(bytes(name)),
                keccak256("1"),
                block.chainid,
                address(this)
            )
        );
}

この例のDOMAIN_SEPARATOR()メソッドは、keccak256 を使用してコントラクト名、chainID、バージョン番号、現在のコントラクトアドレスなどの情報をハッシュ計算して得られるユニークな識別子を生成します。

DOMAIN_SEPARATORの計算が終了した後、abi.encode を使用してこの部分をエンコードします:

abi.encode(
    keccak256(
        "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
    ),
    owner,
    spender,
    value,
    nonces[owner]++,
    deadline
)

ここでのパラメータはすべて permit から渡されたものであり、nonceというフィールドはnonces[owner]++を使用しており、カスタム配列を使用して自動増分のユーザー nonce を維持し、同じ署名のトランザクションが再利用されるのを防ぎます。

このプロセスにより、ハッシュされた署名情報から recoveredAddress アドレスが解析されますが、なぜ解析された recoveredAddress アドレスが owner と一致する必要があるのでしょうか?

ERC-2612 の使用方法#

前述の通り、permit メソッドを呼び出すことで approve の代理呼び出しが完了し、EOA の前置 approve 操作を省略できます。permit を呼び出す際には、非常に重要な 3 つのパラメータ r、s、v が必要であり、これらのパラメータは EOA によって事前に署名されて提供される必要があります。

以下の forge のコードを例にとります:

address alice = vm.addr(1);
bytes32 hash = keccak256("Signed by Alice");
(uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash);
address signer = ecrecover(hash, v, r, s);
assertEq(alice, signer); // [PASS]

alice は hash に署名し、vrs の 3 つのパラメータを返します。次に、ecrecover を使用して signer が alice であることを確認します。

permit の検証ロジックを通過するためには、一致する署名内容を構築して EOA に署名させる必要があります。その後、rsv を持って permit を呼び出すことができます。

以下のコードは permit 署名メソッドのラップであり、Permit 構造体を定義し、erc-2612 の計算方法と一致しています。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract SigUtils {
    bytes32 internal DOMAIN_SEPARATOR;

    constructor(bytes32 _DOMAIN_SEPARATOR) {
        DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR;
    }

    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 public constant PERMIT_TYPEHASH =
        0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

    struct Permit {
        address owner;
        address spender;
        uint256 value;
        uint256 nonce;
        uint256 deadline;
    }

    // permitのハッシュを計算する
    function getStructHash(Permit memory _permit)
        internal
        pure
        returns (bytes32)
    {
        return
            keccak256(
                abi.encode(
                    PERMIT_TYPEHASH,
                    _permit.owner,
                    _permit.spender,
                    _permit.value,
                    _permit.nonce,
                    _permit.deadline
                )
            );
    }

    // ドメインのために完全にエンコードされたEIP-712メッセージのハッシュを計算し、署名者を復元するために使用できる
    function getTypedDataHash(Permit memory _permit)
        public
        view
        returns (bytes32)
    {
        return
            keccak256(
                abi.encodePacked(
                    "\x19\x01",
                    DOMAIN_SEPARATOR,
                    getStructHash(_permit)
                )
            );
    }
}

ERC-2612 のセキュリティ問題#

ERC-2612 は本質的に approve の上に一連の検証ロジックを追加し、ユーザーがオフチェーンでトランザクションの署名を事前に生成し、トランザクション署名の rsv をパラメータとして permit を呼び出すことを許可します。

考えられるセキュリティの脆弱性は以下の通りです:

リプレイ攻撃:DOMAIN_SEPARATORを計算する際に chainID が識別子として使用されます。チェーンが分岐した場合、chainID がコンストラクタで設定されていると、2 つのチェーン上に同じ chainID のコントラクトが存在する可能性があります。これにより、チェーン A で署名し、チェーン B でリプレイすることが可能です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。