3MIKAN
仮想通貨直コン

ERC-1271とは?Safeなどのコントラクト署名を正しく検証する

ERC-1271のisValidSignature、magic value、revert、Safe型thresholdを整理し、EOA・1-of-1・2-of-3・state変更をOpenZeppelinとFoundryで検証します。

3MIKANのブランドキャラクターが複数の署名sealとcontract accountの検証gateを照合する記事画像

ERC-1271は、contract account自身へ「このdigestとsignatureは、今このaccountにとって有効ですか」と問い合わせる標準です。

EOAは秘密鍵で署名するため、ECDSAで復元したaddressを期待するsignerと比較できます。一方、Safeのようなcontract accountはcontract自体の秘密鍵を持ちません。複数owner、threshold、session key、pauseなどの現在stateを使って有効性を判定します。

そのため、すべてのsignerへecrecoverだけを使う実装はcontract accountの正しい署名を拒否します。反対に、「contractだから有効」と扱うこともできません。ERC-1271のcallが成功し、厳密なmagic valueを返したかまで確認します。

この記事ではOpenZeppelin Contracts 5.6.1SignatureCheckerを使い、EOA、1-of-1、2-of-3、wrong owner、threshold不足、署名順序、wrong digest、期限切れ、wrong magic、revert、state変更をFoundryのlocal環境で再現します。実在Safe、外部RPC、wallet接続、mainnet署名、実資産は使いません。

EOA署名とcontract署名の違い

最初に、検証する主体を分けます。

signer 誰が署名の有効性を決めるか 基本の検証 結果が後から変わるか
EOA 1つの秘密鍵 digestとsignatureからECDSAでaddressを復元 同じdigest・signature・addressなら通常は変わらない
deployed contract account account contractの現在のlogicとstate isValidSignature(digest, signature)staticcall owner、threshold、pauseなどで変わり得る
undeployed counterfactual account 将来deployされるaccountとfactory情報 ERC-1271だけでは直接検証できない ERC-6492などdeploy-awareな経路が必要

ここで検証するのは「このsignerにとって、このdigestのsignatureが有効か」です。署名対象が安全か、同じ署名を再利用できないか、期限内か、処理が成功するかは別の確認です。

ERC-1271のinterfaceとmagic value

ERC-1271の中心は次のinterfaceです。

interface IERC1271 {
    function isValidSignature(bytes32 hash, bytes calldata signature)
        external
        view
        returns (bytes4 magicValue);
}

有効なときに返す値は、次の4 bytesです。

bytes4(keccak256("isValidSignature(bytes32,bytes)"))
= 0x1626ba7e

true1、空のreturn data、似た別の4 bytesでは成功になりません。呼び出しがrevertした場合も、有効なsignatureとして扱いません。

ERC-1271の仕様では、この関数はstateを変更してはいけません。ただし、stateを読んで判定することは禁止されていません。たとえば現在のowner集合、必要署名数、時刻、許可済みhashを読めます。外部callも許されるため、実装ごとの処理量を想定せず、独自の小さなgas上限を決め打ちしないことも重要です。

signerのcode有無に応じてEOAのECDSA検証とcontract accountのERC-1271検証へ分かれる流れ

OpenZeppelin SignatureCheckerで検証入口を揃える

OpenZeppelinのSignatureCheckerは、address型のsignerについて次の分岐を行います。

  • signerにcodeがない: ECDSAで復元したaddressとsignerを比較
  • signerにcodeがある: ERC-1271のisValidSignaturestaticcall

利用側は次のように1つの入口へまとめられます。

import {SignatureChecker} from
    "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

function isValid(
    address signer,
    bytes32 digest,
    bytes calldata signature
) public view returns (bool) {
    return SignatureChecker.isValidSignatureNowCalldata(
        signer,
        digest,
        signature
    );
}

この記事のsampleは@openzeppelin/contractsをexact versionの5.6.1へ固定しています。major versionだけ、^5.6.1latestではありません。signature validationはsecurity境界になるため、更新時はchangelogとtest結果を別途確認します。

SignatureCheckerはcontract callがrevertした場合、return dataが短い場合、返り値が0x1626ba7eでない場合をfalseへまとめます。利用APIをboolに揃えられますが、backendで障害を調べるときは、低level callの成功、return data、対象blockも別に記録すると原因を分けられます。

code.length == 0はEOAの証明ではない

この分岐は便利ですが、「今codeがないaddressは必ずEOA」という身元証明ではありません。

  • まだdeployされていないcounterfactual account
  • constructor実行中のcontract
  • 将来同じaddressへdeployされる前の状態

なども、その時点ではcodeが見えない場合があります。さらにEIP-7702で委任codeを持つEOAは、従来の「EOAにはcodeがない」という前提から外れます。

したがって、code.length検証methodを選ぶ現在stateの入力として扱い、account種別や永続的な本人性の結論にはしません。

local sampleで1-of-1を検証する

再現環境は次の通りです。

項目 version・設定
OpenZeppelin Contracts 5.6.1 exact pin
Solidity 0.8.36
Foundry 1.8.0
chain / RPC Foundry内のlocal EVMのみ
key / account deterministic local test keyとdeployした検証用contractだけ
asset transfer なし

Threshold1271Walletはowner addressとthresholdを保存し、signature payloadを次の独自形式で受け取ります。

abi.encode(
    address[] signers,
    bytes[] ownerSignatures
)

1-of-1では、1人のownerが同じdigestへ署名し、wallet contractがowner membershipとECDSA signatureを確認します。条件を満たせば0x1626ba7eを返し、外側のSignatureCheckertrueになります。

bytes memory signature = abi.encode(signers, ownerSignatures);

require(
    wallet.isValidSignature(digest, signature)
        == IERC1271.isValidSignature.selector
);

require(verifier.isValid(address(wallet), digest, signature));

このpayloadは学習用sampleの形式で、Safeのpacked signature形式ではありません。production accountへそのまま渡せる互換形式だとは考えないでください。

2-of-3のthresholdと署名順序を確認する

2-of-3では3人のownerのうち2人以上が、同じdigestへ署名する必要があります。sampleは次を順番に確認します。

  1. signature数がthreshold以上か
  2. signer数とsignature数が一致するか
  3. signerが現在のownerか
  4. signer addressが厳密な昇順か
  5. 各signatureが同じdigestに対して有効か

Safe本体も必要数のsignatureを確認し、owner addressが前のownerより大きいことを要求して、無効ownerとduplicateを拒否します。SafeのGS020はsignature data不足、GS026は無効・重複ownerや順序などを調べる目印です。

ただし、この記事のThreshold1271Walletowner・threshold・順序・state依存だけを切り出したSafe型の最小再現です。Safeのcontract signature、approved hash、P-256、module、executor context、実際のpacked encodingを再実装したものではありません。資産管理には使えません。

3人のownerから2署名を検証し、owner state変更後に同じ署名が無効になる流れ

正常系と失敗系をFoundryで再現する

local sampleは、成功だけでなく同じ入口から次のfailureを作ります。

test 入力・state 期待結果 主に分かること
EOA success ownerのkeyで正しいdigestへ署名 true ECDSA経路
EOA wrong signer 別ownerとして検証 false address一致が必要
1-of-1 現owner 1人の正しい署名 magic / true ERC-1271正常系
2-of-3 現owner 2人を昇順で提出 magic / true threshold正常系
insufficient threshold 2-of-3へ1署名だけ提出 false signature数不足
wrong owner outsiderを含める false owner membership
unsorted 正しい2人を降順で提出 false 順序・duplicate防止
wrong digest signature後にdigestを変更 false 全ownerが同じdigestへ署名する必要
expired application deadline後に検証 false 期限はapplication側の条件
wrong magic contract call成功、0xffffffffを返す false call成功だけでは不足
revert isValidSignatureがrevert false revertを承認にしない
owner change ownerを外して同じsignatureを再検証 false state依存
validation pause walletを停止して同じsignatureを再検証 false block間で結果が変わる
undeployed account codeのない予定address false ERC-6492境界

repository rootで次を実行します。

npm ci
CI_REQUIRE_FOUNDRY=1 npm run test:contracts

完全なcontractとtestは3MIKANの公開repositoryで確認できます。mainnet / testnet RPC環境変数は使いません。

deadlineはERC-1271の自動機能ではない

local sampleの期限確認は、統一verifier側へ明示的に追加しています。

function isValidBefore(
    address signer,
    bytes32 digest,
    bytes calldata signature,
    uint256 deadline
) external view returns (bool) {
    return block.timestamp <= deadline
        && SignatureChecker.isValidSignatureNowCalldata(
            signer,
            digest,
            signature
        );
}

ERC-1271へdeadline引数はありません。deadlineを署名対象へ含めず、別入力としてだけ受け取ると、提出者が値を変えられる設計になり得ます。実際のapplicationではdeadline、nonce、action、chain、検証先をdigestへ結びつけたうえで、現在stateも確認します。

contract signatureは「今」の答え

OpenZeppelinの説明にある通り、contract signatureはblock Nでtrue、block N+1でfalseになる場合があります。逆に、ownerが追加されたり事前承認stateが変わったりして、後から有効になる実装も考えられます。

viewは「このcall自身がstateを変更しない」という意味です。返り値が永続的に固定される意味ではありません。

off-chain backendで検証結果を証拠として残す場合は、少なくとも次を一緒に記録します。

  • chainId
  • signer contract address
  • block numberまたはblock tag
  • digest
  • signatureまたはその安全な参照
  • low-level callの成功・revert
  • return value
  • 検証時点のaccount implementation / codeを追える情報

長期sessionや注文を有効にする場合は、owner変更、account upgrade、pause、nonce消費などの後に再検証する条件も決めます。「一度trueだったsignature」を無期限の本人証明としてcacheしません。

EIP-712はdigest、ERC-1271はsignerを担当する

ERC-1271へ渡す最初の引数は、意味の説明がないbytes32 hashです。ERC-1271だけでは、その値が注文、投票、ログイン、Permitのどれか分かりません。chainId、verifyingContract、nonce、deadlineが入っている保証もありません。

役割を分けると次のようになります。

主な役割 自動では保証しないもの
EIP-712 type、domain、messageから最終digestを作る nonce消費、deadline判定、effectの正当性
ERC-1271 contract accountがdigestとsignatureの現在有効性を答える digestの意味、replay防止、transaction成功
application nonce、deadline、権限、effect、state遷移を検証する account内部のowner判定

domain separator、hashStruct(message)、nonce、deadlineを同じdigestへ結びつける手順は、EIP-712署名の読み方とreplay対策でlocal codeと一緒に確認できます。

ABIからisValidSignatureのselector、引数、return dataを追う場合は、ABI・function selector・event logsの確認方法を参照してください。

EIP-7702とcounterfactual accountの境界

EIP-7702ではEOA addressがdelegation codeを持てます。そのaddressをSignatureCheckerへ渡すと、現在codeがあるためERC-1271側へ進みます。delegated implementationがaccountの文脈でERC-1271を正しく提供しているかが必要です。

address、保存領域、delegation code、msg.senderの境界は、EIP-7702でEOAはどう変わるかでFoundry testを使って確認できます。

反対に、counterfactual accountがまだdeployされていなければisValidSignatureをcallできません。ERC-6492は、factoryとdeploy calldataをsignatureへ包み、必要ならdeploy処理を考慮してからERC-1271を試す検証順を定義します。

ERC-6492はdeploy side effect、wrapper解析、reentrancyなど追加の境界を持つため、この記事のsampleには混ぜていません。未deploy accountへ単純なcode.length → ECDSAだけを使うのではなく、counterfactual signatureに対応するvalidatorを別途設計・検証します。

backend・contract reviewチェックリスト

  • 期待するsigner addressをdigest外の入力と混同していない
  • EOAはECDSA、deployed contractはERC-1271で検証する
  • ERC-1271 call成功だけでなく0x1626ba7eを厳密に確認する
  • wrong magic、短いreturn、revertを有効として扱わない
  • code.length == 0を永続的なEOA証明にしない
  • 使用するsignature libraryのversionをexact pinした
  • account固有のsignature encodingとthreshold規則を公式sourceで確認した
  • Safe型signatureではowner順序とduplicateを確認した
  • EIP-712などでchain、contract、action、nonce、deadlineをdigestへ結びつけた
  • nonce消費とdeadline判定をapplication stateで行う
  • 検証blockとaccount stateを記録し、変更後の再検証条件を決めた
  • counterfactual accountはERC-6492等の別経路として扱う
  • signature有効をtransaction成功、安全性、監査済みの証明にしない

ERC-1271対応で大切なのは、「contract walletも受け付ける」という一行を足すことではありません。EOAのECDSA、contract accountの現在state、digestを作るapplication規則を同じ認可フローの別レイヤーとして照合することです。

この記事にスポンサー、affiliate、wallet作成、資産移動のCTAはありません。将来、署名基盤の開発相談やsponsor表示を置く場合も、広告であることを明示し、上の仕様・test結果・review結論から独立させます。production実装には、このsampleとは別の設計review、account固有test、脅威分析が必要です。

確認した一次情報