3MIKAN
仮想通貨直コン

Safeの取引が進まない時の確認手順:署名・threshold・nonce・実行失敗を分ける

Safe multisigが署名待ち、nonce競合、実行失敗になるとき、proposal、owner confirmations、Safe nonce、guard、target revertを証拠ごとに確認します。

3MIKANのブランドキャラクターが三つのowner keyと二つの鍵穴を持つ閉じた金庫を確認し、署名収集と実行を分けて考えるSafe記事の画像

Safeの画面に「Awaiting confirmations」「Queued」「Execution failed」などと出ても、同じ原因とは限りません。最初に分けるのは、proposalが作られたことowner signaturesが集まったこと別のEthereum transactionでon-chain executionが試されたことの3段階です。

2-of-3で署名が2件見えても、現在のowners・threshold、署名対象のSafe transaction hash、Safe nonce、guard、target callが一致しなければ実行成功は確定しません。反対に、Transaction Serviceの表示が遅れていても、chain上ではすでにSafe nonceが進んでいる場合があります。

この記事は、Safe addressやchainを確認せずに再署名・再実行する手順ではありません。公開dataはread-onlyで扱い、掲載する再現結果はlocal chainだけで動かしています。実在Safeへの接続、署名要求、module・guard変更、transaction送信、資産移動は行いません。

先に止める操作と保存する値

原因が分かるまでは、同じ内容への再署名、nonceだけを変えた再提案、未知のexecutorやrelayerへの送信、guard・module・thresholdの変更、検索結果やDMから開く「recovery」接続を止めます。ownerであっても、内容を確認していないhashへ署名しません。

一件の調査メモへ次を保存します。

項目 保存する値 分けたいもの
Safe identity chain ID、Safe address、implementation、VERSION()、確認block 別chain・別proxy・別version
authorization owners、threshold、current Safe nonce 現在の署名条件
proposal to、value、data、operation、gas関連field、nonce、safeTxHash 署名対象の内容
confirmations signer address、signature type、確認時刻、対応safeTxHash 件数表示と有効署名
execution Ethereum tx hash、sender、EOA nonce、receipt、logs、trace proposalとon-chain attempt
extensions modules、transaction guard、module guard、fallback handler owner経路以外の実行・拒否
service Transaction Service URL、response、indexing status、確認時刻 off-chain recordとchain state

Safe addressとchain IDを固定できない場合はそこで停止します。addressの文字列が同じでもchainが違えば別のcontract stateです。proxy implementationの読み方はERC-1967 proxyの確認手順も参照してください。Safe proxyはERC-1967 slotを使うとは限らないため、一般的なproxy判定をそのまま当てはめず、対象versionの公式sourceと照合します。

proposal・confirmations・executionの3段階

off-chain proposal、owner confirmations、thresholdと、別のon-chain execTransaction、receipt、Safe event、traceを分ける図

1. proposal

proposalは、Safe transactionのfieldとSafe nonceをまとめ、EIP-712のsafeTxHashを作った段階です。Safe Transaction Serviceへ登録されたproposalや、別のcoordinatorが保持するproposalはoff-chain recordです。proposalが一覧にあるだけでは、Safe contractのstateは変わりません。

2. confirmations

ownerは、表示ラベルではなく同じsafeTxHashへ署名します。Safe v1.5.0の通常のEOA署名は、recovered owner addressの昇順に連結して検証されます。2-of-3なら現在のowner 3名のうち2名が必要ですが、削除済みowner、別hash、重複owner、順序不正、threshold未満は有効な2件になりません。

ownerがcontract accountならERC-1271など別のsignature validationが関係します。EOA signatureとcontract signatureの違いはERC-1271の検証手順、wallet promptでtyped dataのdomain・chain・contract・nonceを読む方法は署名要求の安全確認へ分けてください。

3. on-chain execution

thresholdへ到達しても自動実行とは限りません。executorが別のEthereum transactionでSafeのexecTransactionを呼びます。このouter transactionにはEthereum tx hashとexecutor EOA nonceがあり、Safe transactionにはsafeTxHashとSafe nonceがあります。

receiptのstatus: 1はouter callがrevertせず終わったことを示します。しかしSafeがtarget failureをcatchしてExecutionFailureをemitし、execTransactionfalseを返す構成では、outer receiptが成功でもSafe内部callは失敗し得ます。receipt、ExecutionSuccess / ExecutionFailure event、return value、traceを同じEthereum tx hashへ対応させます。

safeTxHashとEthereum tx hashは別物

identifier いつ作られるか 主な入力 何を証明しないか
Safe transaction hash proposal作成時 chain ID、Safe address、to、value、data、operation、gas / refund field、Safe nonce broadcast、mining、target success
Ethereum transaction hash executorがnetworkへ送る時 outer transaction、executor、EOA nonce、gas、chain Safe内部callの成功、署名内容の妥当性

Safe v1.5.0のgetTransactionHashへ渡すfieldは次の10個です。

to, value, data, operation,
safeTxGas, baseGas, gasPrice, gasToken, refundReceiver,
nonce

どれか1つが変われば別のSafe transactionです。Safe addressとchain IDもEIP-712 domainへ入るため、別Safe・別chainのhashへ署名を流用できません。公開検証JSONは、架空Safe 0x0000…5000、chain ID 31337、Safe nonce 7を固定し、value 41と42のproposalが異なるsafeTxHashになることをoffline計算しています。generatorはprivate key、wallet、RPC、signature、transactionを使いません。

Transaction ServiceのsafeTxHashがある場合も、取得したtransaction fieldと対象SafeのgetTransactionHashから再計算して一致を確認します。見た目が同じ送金先やamountでも、data、operation、refundReceiver、nonceが違えば同じhashではありません。

Safe nonceとexecutorのEOA nonceを分ける

Safe nonceは、Safeがowner-authorized transactionを順序付けしてreplayを防ぐ値です。executor EOA nonceは、outer transactionを送るaccount自身のnetwork nonceです。同じ7と表示されても、所属するaccountと役割が違います。

同じSafe nonce Nを使うproposal AとBが別hashになり、A実行後はN+1となってBの元署名がstaleになる図

同じSafe nonce Nでproposal AとBが作られることはあります。AとBはdataが異なるのでsafeTxHashも別です。Aが先に成功またはSafeがfailureをcatchしてnonceを消費すると、current Safe nonceはN + 1になります。Bへ集めた署名はnonce Nのhashへ固定されているため、そのままN + 1へ付け替えられません。

UIの「reject」「replace」は、元proposalを消去する魔法ではありません。どのSafe transactionがどのnonceを使い、どのhashがon-chainで実行され、current nonceがいくつになったかを確認します。新しいnonceへ内容を移すなら新しいsafeTxHashとなるため、ownersは新しい全fieldを再確認します。

executor側でnonce too lowが出た場合は、Safe nonceではなくexecutor EOAのpending transaction競合かもしれません。outer sender、EOA nonce、replacement fee、Ethereum tx hashを別列で調べます。

owners・threshold・modules・guards・versionをread-onlyで確認する

UIやTransaction Serviceのcacheより先に、対象blockのSafe contractをread-onlyで確認します。RPC、Explorerのverified contract read、または監査済みscriptを使い、wallet接続やwrite methodは不要です。

対象 Safe v1.5.0で読む入口 注意点
version proxy経由のVERSION() implementationが違えば挙動を一般化しない
owners getOwners() 表示名ではなくaddressを保存
threshold getThreshold() confirmations数と同じ時点で読む
Safe nonce nonce() proposalのnonceとcurrent値を分ける
modules getModulesPaginated(0x…01, pageSize) sentinelからpaginationし、全pageを確認
transaction guard getStorageAt(GUARD_STORAGE_SLOT, 1) v1.5.0はpublic getterを持たない
module guard getStorageAt(MODULE_GUARD_STORAGE_SLOT, 1) owner transaction guardとは別
implementation Safe proxyのmasterCopy()またはstorage slot 0 対象proxy typeを公式sourceで固定

Safe v1.5.0のtransaction guard slotは次です。

0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8

module guard slotは次です。

0xb104e0b93118902c651344349b610029d694cfdec91c589c91ebafbcd0289947

getStorageAt(uint256(slot), 1)が返す32 bytesの末尾20 bytesをaddressとして読みます。これらはSafe v1.5.0のpinned sourceで確認した値です。別versionへ無条件に流用しません。guardやmoduleを「確認のため」に無効化・交換しないでください。設定変更自体がowner-authorized writeであり、資産controlの境界を変えます。

moduleは、通常のowner threshold transactionとは別のexecTransactionFromModule経路を使えます。有効なmoduleが実行したtransactionでは、通常proposalのconfirmation件数を原因にしません。呼び出し元module、module guard、module event、traceを確認します。悪意あるmoduleは任意実行につながり得るため、名称やUI iconではなくaddress、code、設定transactionを記録します。

failureを止まった場所で分類する

threshold不足、nonce競合、guard reject、target revert、Transaction Service lagを止まる場所、Safe nonce、次に読む証拠で分ける表

threshold不足・invalid owner・署名順序

Safeはnonceをpost-incrementしてからsignature検証へ進みますが、signature検証がrevertすればouter call全体がrollbackするため、Safe nonceは消費されません。次を同じblockとsafeTxHashで確認します。

  • current ownersとthreshold
  • 各signatureからrecoveryしたowner、またはcontract signature owner
  • signatureが同じsafeTxHashへ対応するか
  • EOA signaturesがowner address昇順か
  • prevalidated signatureやERC-1271などsignature typeの解釈

件数だけ合わせるために追加署名を依頼せず、まず既存signatureがどのhashへ対応するかを固定します。

transaction guard reject

transaction guardはexecTransactionのtarget call前後で検査を実行できます。事前検査がrevertするとtargetは呼ばれず、outer callがrevertするためnonce incrementもrollbackします。guard revert data、guard address、guard code version、blocked conditionを保存します。

guardを外せば通るという案内はしません。Safe公式も、壊れたguardや悪意あるguardがtransactionをblockできると注意しています。guard変更はSafe自身からのauthorized callが必要で、別の運用・security reviewです。

target revertとExecutionFailure

signatureとguardを通過しても、target contractがrevertする場合があります。Safe v1.5.0ではsigned gas fieldによりouter結果が変わります。

  • safeTxGas > 0またはgasPrice > 0でtarget failureをSafeがcatchした場合、execTransactionfalseを返してExecutionFailureをemitし、Safe nonceは消費されます
  • safeTxGas == 0かつgasPrice == 0ではtarget failureをbubbleするため、outer transactionがrevertし、Safe nonce incrementもrollbackします

したがって「target revertならnonce未消費」「receipt successならSafe success」のどちらも一般化できません。signed field、Safe event、trace、current nonceを一緒に読みます。

target revertの原因はSafeではなく、target側のallowance、role、deadline、slippage、paused state、calldata、value、delegatecall contextなどにある場合があります。revert dataとtraceを保存し、同じcallを実資産環境で闇雲に再実行しません。

execution gas・refund・relayer

safeTxGasbaseGasgasPricegasTokenrefundReceiverはsafeTxHashに含まれます。画面でgas estimateだけを更新したつもりでも、signed Safe transactionが変わる場合があります。

gasPrice > 0ならSafeは実装の計算に基づきrefund paymentを試みます。native refundやtoken transferが失敗するとouter executionもrevertし得ます。executorが払うnetwork fee、Safeが設定に基づき返すpayment、relayer serviceの料金は別項目です。refundReceiverやgasTokenを推測で変えず、署名対象field、Safe balance、token behavior、outer receiptを確認します。

Transaction Service表示とon-chain stateがずれる場合

Safe Transaction Serviceはoff-chain proposalとconfirmationsを提供し、chainのtraceやeventsをindexします。便利なcoordination layerですが、Safe contractそのものではありません。indexer lag、unsupported chain、trace取得失敗、reorg、service endpointの違いにより、一時的に表示とchain stateがずれる可能性があります。

次の順で照合します。

  1. RPCでchain ID、Safe code、implementation、versionを固定する
  2. owners、threshold、current Safe nonce、modules、guardsを同じblockで読む
  3. service responseのtransaction fieldsからsafeTxHashを再計算する
  4. execution tx hashがあればRPCでtransactionとreceiptを取得する
  5. Safe eventとtraceを読み、target callのsuccess / revertを確認する
  6. 最後にTransaction Serviceのindexing status、response時刻、再取得結果を比較する

serviceにproposalがないことは、on-chain executionがなかった証拠ではありません。反対に、serviceにconfirmationsが揃っていることも、実行済みの証拠ではありません。on-chain stateを先に固定し、Transaction Serviceのstatusは最後に照合することが切り分けの基準です。

local 2-of-3で再現した結果

公式@safe-global/[email protected]をlocal Foundry chainへdeployし、公開された検証専用keyだけを使って次の条件を再現しました。外部RPCやwalletへは接続していません。

条件 local実行の結果
2-of-3 owner 2名のsorted ECDSA signaturesでtarget stateを更新し、Safe nonceが1進む
threshold不足 1 signatureでrevertし、target stateとSafe nonceは変わらない
invalid owner owner + outsiderでrevertし、Safe nonceはrollbackする
nonce競合 同じnonceのAを実行後、Bの元signatureはcurrent nonceと合わず実行できない
target revert non-zero safeTxGasfalseを返し、target stateは変わらずSafe nonceは進む
guard reject Safe self-callで設定したguardがtarget前にrevertし、Safe nonceはrollbackする

同じlocal実行でtransaction guardを公式storage slotからread-only確認し、module一覧が空であること、VERSION()1.5.0であることも確かめました。この結果はSafe v1.5.0の限定条件を再現したもので、任意のSafe、module、guard、target、relayerで同じ結果を保証しません。

support・運用担当へ渡すpackage

問い合わせには、screenshotだけでなく次のmachine-readableな値を渡します。seed phrase、private key、full signature blobを公開ticketへ貼りません。

  • chain ID、Safe address、implementation address、Safe version、確認block
  • owners、threshold、current Safe nonce
  • proposalの全transaction fieldsとsafeTxHash
  • signer addressesとconfirmation時刻。signature本体は必要性と安全な共有先を確認する
  • execution Ethereum tx hash、outer sender、EOA nonce、receipt、Safe event、trace、revert data
  • modules、transaction guard、module guard、fallback handler
  • Transaction Service endpoint、response、indexing status、取得時刻
  • 期待した結果と実際の結果、最初に不一致になった段階

この順に揃えると、「署名が足りない」「同じnonceの別proposalが先に進んだ」「guardが拒否した」「targetがrevertした」「service表示だけ遅れている」を混ぜずに相談できます。safeTxHashとEthereum tx hash、Safe nonceとexecutor EOA nonceを別欄にすることが、再署名や再実行を判断する前提です。

最終チェック

  1. chain ID、Safe address、implementation、VERSION()を固定した
  2. owners、threshold、Safe nonce、modules、transaction guard、module guardを同じblockで読んだ
  3. proposalの全fieldからsafeTxHashを再計算した
  4. confirmationsが同じhashと現在のownersへ対応することを確認した
  5. 競合proposalごとにnonceとsafeTxHashを分けた
  6. execution tx hash、executor EOA nonce、receipt、Safe event、traceを保存した
  7. target revert、guard reject、refund failure、service lagのどこで止まったかを分類した
  8. 原因確定前の再署名、再提案、module・guard変更、資産操作を止めた

この8項目が揃うまで、UIのstatusだけを根拠に「署名を追加すれば進む」「rejectすれば置換できる」「もう一度実行すれば直る」と判断しません。

確認した一次情報