本文へ移動
SafeとWeb3の運用

デジタル資産を扱うAIエージェントのための権限マトリクス

権限マトリクスは、AIエージェントが何を準備し、人が何を承認し、どのチャネルが実行するかをアクションごとに定めます。

公開日 更新日 10 分で読める
permissionsSafeAI agentsWeb3 opsmultisig

この記事は教育目的であり、正式なスマートコントラクト監査、法的助言、金融助言、またはプロトコル固有のセキュリティレビューに代わるものではありません。

デジタル資産を扱うAIエージェントには、ツールの一覧以上のものが必要です。エージェントが何を読み取り準備できるか、人が何を承認するか、どの承認済みチャネルが実行するかを、アクションごとに定めた権限マトリクスが必要です。

琥珀色の錠が縦に並んだ列の一つで手が鍵を回している。コンソールには青いファイルのアイコン、トグルの格子、ユーザープロフィールのタイルがある。
錠の一つひとつが、それぞれ担当者を持つ別々の権限。コンセプトイラスト。

役に立つ単位はツールではなく能力です。署名、鍵へのアクセス、モジュールの有効化、フォールバックハンドラーの変更、DELEGATECALL 操作の承認は、どれもアカウントに対する権限を開くため、それぞれに専用の行、専用の承認、専用の担当者が必要です。

この記事では、その考え方を、Safe ベースの運用フローを回す小規模チーム向けのマトリクスに落とし込みます。扱うのは、各行の内容、各権限の背後にあるリソース、カストディ事業者のポリシーエンジンがカバーする範囲とチームに残る範囲、そしてエージェントが本番環境に触れる前にマトリクスを検証するネガティブケースです。

本番環境におけるエージェント権限の一般原則については、下のリンク先の記事を参照してください。このマトリクスは、鍵、署名、モジュール、ガード、そしてそれらを迂回する経路に絞っています。

権限を開く能力から始める

ツールの名前はリスクについてほとんど何も語りません。propose という名前のツールは、payload の下書きを作るだけかもしれませんし、それに署名する鍵を持っているかもしれません。そのため、マトリクスはまず能力を列挙し、そのあとでツールを対応させます。

Safe のスマートアカウントでは、次の能力が、誰が資金を動かせるか、あるいはアカウントがどう振る舞うかを変えます。

  • トランザクションへの署名や送信、またはそれを行う鍵の保持。
  • オーナーの追加や削除、しきい値の変更。
  • モジュールの有効化や無効化。Safe のドキュメントによれば、モジュールは任意のトランザクションを実行でき、悪意のあるモジュールは Safe を乗っ取ることができます。
  • トランザクションガードの設定、または Safe 1.5.0 以降のモジュールガードの設定。ドキュメントによれば、不具合のあるガードは Safe のサービス拒否を引き起こすことがあります。
  • フォールバックハンドラーの変更。ドキュメントは、これによってアカウント内で任意の外部関数が実行可能になると注意しています。
  • operation フィールドが DELEGATECALL のトランザクションの承認。Safe は CALL と DELEGATECALL をサポートしており、DELEGATECALL では呼び出し先のコードが Safe のコンテキストで、Safe のストレージと残高を使って実行されます。
  • プロトコル自身のコントラクトのアップグレード、一時停止、パラメータ変更。

マトリクスに影響するバージョンの違いが一つあります。ガードが導入された Safe 1.3.0 から 1.4.1 までは、トランザクションガードがチェックするのは execTransaction で実行されるトランザクションだけで、モジュールが実行するトランザクションは対象外です。Safe 1.5.0 ではこの経路向けに独立したモジュールガードが追加されましたが、設定しない限り機能しません。1.5.0 にアップグレードしただけ、あるいはトランザクションガードだけを設定した状態では、モジュールはチェックされないままです。どちらもローカルフォークで確認しました。すべての呼び出しを拒否するガードを設定した Safe 1.4.1 では、オーナーが署名したトランザクションはリバートし、モジュールのトランザクションはそのまま通りました。Safe 1.5.0 では、モジュールガードを設定して初めてモジュールのトランザクションがリバートしました。

マトリクスを一行ずつ

要するに、エージェントは読み取り、デコードし、シミュレーションし、下書きを作ります。人は正確な payload や具体的な変更を承認します。エージェントの外にある承認済みチャネルが署名し実行します。以下の各行は、エージェントにできること、承認する人、実行するものの三つで構成されます。

  • 残高と設定の参照。エージェント:一覧にあるネットワークと Safe に限った型付きの読み取り。承認:事前に合意した読み取りポリシー。実行:読み取り専用ツール。
  • デコードとシミュレーション。エージェント:特定のブロックに固定した根拠を用意する。承認:人がカバー範囲と結果を確認する。実行:承認済みのシミュレーター。
  • 送金の準備。エージェント:未署名の下書き。承認:署名者が正確な payload を確認する。実行:エージェントの外にある承認済みチャネル。
  • 署名または送信。エージェント:対象外。承認:人による署名手順。実行:ウォレットまたは承認済みチャネル。
  • 鍵のローテーション、オーナーの変更、しきい値の変更。エージェント:記録に残る提案。承認:変更の担当者と署名者。実行:管理用チャネル。
  • モジュール、ガード、フォールバックハンドラーの変更やアップグレード、またはあらゆる DELEGATECALL への署名。エージェント:分析と下書き。承認:その変更に特化したレビュー。実行:管理用チャネル。
  • コントラクトの一時停止やパラメータ変更。エージェント:診断と提案。承認:手順で指名された担当者。実行:制限された外部ツール。

似て見えて同じではない行が二つあります。送金の準備も DELEGATECALL の承認も、最後は execTransaction の呼び出しになり得ますが、後者はアカウントそのものを変えることがあります。だからマトリクスは、それをモジュールやガードの変更と同じレビューに回します。

各権限の背後にあるリソースを明記する

行を実際に適用できるのは、そのリソースが明記されているときだけです。権限ごとに、チームはネットワーク、Safe、トークンコントラクト、承認済みの受取先、エージェントが下書きできる関数、適用される上限を書き出します。

例外も同じです。あるフローで DELEGATECALL が必要な場合、たとえば呼び出しをまとめるためなら、マトリクスはレビュー済みの呼び出し先コントラクトとそのバージョンを明記し、それ以外の DELEGATECALL の呼び出し先はエージェントの下書きに入れません。ある連携でモジュールが必要なら、マトリクスにはモジュールのアドレス、担当者、取り外す予定日を書きます。

カストディ事業者のポリシーエンジンがカバーするもの

チームによっては、独自のポリシーエンジンを持つカストディ事業者やウォレット基盤事業者、たとえば Turnkey や Fireblocks が保管する鍵にエージェントを接続します。これらのエンジンは、自分に届いたリクエストにルールを適用します。マトリクスには、各エンジンがどの行を適用し、どの行が別のコンポーネントに依存するかを記録しておく必要があります。

Turnkey のドキュメントによれば、組織内のすべてのポリシーがリクエストごとに評価されます。一致する deny があればそれが優先され、なければ一致する allow が適用され、それ以外は暗黙に拒否されます。Ethereum のルールは、受取先、金額、calldata といった解析済みのトランザクションフィールドを読めます。関数名と引数はコントラクトのインターフェースをアップロードした場合にのみ使えます。型付きデータも評価できます。同じドキュメントには、root quorum が承認したアクションはポリシーエンジンを通らず、どのポリシーでも制限できないと書かれています。

Fireblocks のドキュメントによれば、同社の Policy Engine は Fireblocks Console または API から作成されたすべてのトランザクションにポリシーを適用します。ルールは上から順に最初に一致したものが適用されるため、順序が重要です。コントラクト呼び出しのルールはメソッドで絞り込めます。ポリシーを管理できるのは workspace の Owner か Admin レベルのユーザーだけで、提案された変更は承認グループの承認が必要です。

Safe には、事業者のエンジンではなくコントラクトが定める権限の経路が加わります。

  • あるエンジンが管理する鍵は、複数いるオーナーの一人にすぎないことがあります。ほかのオーナーの鍵は、別の場所で別のルールのもとで署名します。
  • 有効化されたモジュールは execTransactionFromModule を通じて実行され、この経路では Safe はオーナーの署名を検証しません。検証があるとすれば、それはモジュール自身が行うものです。事業者のエンジンがこの実行を目にするのは、モジュールを呼び出す鍵、またはモジュールでの実行を承認する鍵をそのエンジンが管理している場合に限られます。その場合でもエンジンに届くのはモジュール宛てのリクエストであって、オーナーが署名した Safe のトランザクションではありません。そのため、Safe のトランザクションを想定して書いたルールは、このリクエストには適用されません。
  • ガードとフォールバックハンドラーは Safe 自体に設定されます。トランザクションガードが設定されていれば、どの鍵で署名されたかにかかわらず、すべての execTransaction をチェックしますが、モジュールが実行する内容はチェックしません。Safe 1.5.0 では、この経路に専用のモジュールガードが必要です。フォールバックハンドラーは、Safe が実装していない関数への呼び出しに応答します。さらに、有効化されたモジュールはオーナーの署名なしにこれらをすべて変更できます。いずれも Safe が自分自身に対して行う呼び出しだからです。
  • DELEGATECALL は operation フィールドに含まれます。このフィールドは、オーナーが署名する SafeTx、通常は EIP-712 の型付きデータにも、それを送信する execTransaction の呼び出しにも入っています。ルールで止められるのは、エンジンが受け取る形式でこのフィールドを読む場合だけです。鍵が SafeTx のハッシュだけに署名する場合、エンジンが読めるフィールドは一つもありません。

事業者のエンジンはマトリクスの中の一つの適用ポイントであり、マトリクスそのものではありません。Safe のオーナーの鍵では、エンジンが読む受取先はたいてい Safe 自体です。実際の送付先、金額、ガス代の払い戻しのフィールドは calldata や型付きデータの中にあるため、ルールがそれらをデコードしない限りチェックされません。どのルールを書くか、誰がそれを変更できるか、root quorum、モジュール、ほかのオーナーなど、どの経路がルールを迂回するか、そしてそれらの経路をネガティブケースでどう試すかは、引き続きチームが決めます。

独自の判断が必要な新しいモジュール

説明用のシナリオです。ある決済連携が、定期送金を実行するためのモジュールを有効化するようチームに求めます。エージェントはモジュールのコードとドキュメントを読み、それが Safe で呼び出す関数を列挙し、enableModule のトランザクションを下書きできます。有効化を決めることはできません。

モジュールの有効化と無効化は、どちらも Safe が自分自身に対して行う呼び出しです。しきい値分の署名があるトランザクションか、すでに有効なモジュールを経由します。いったん有効化されると、誰かが disableModule を実行するまでモジュールは動作できます。チームのツールから連携を外しても、Safe からは外れません。Safe 1.4.1 までは、モジュールが実行する内容をチェックするガードはありません。1.5.0 ではモジュールガードだけがチェックし、それも設定されている場合に限られます。そのため、マトリクスでは有効化、その後の取り外し、ガードの扱いを、それぞれ担当者を決めた三つの別々の判断として扱うべきです。

ネガティブケースでマトリクスを確かめる

マトリクスが役に立つのは、エージェントを取り巻くツールがそれを適用するときだけです。最初のチェックでは、誰かが署名する前に、すべての下書きをどれかの行に振り分けます。この例では bash と Foundry の cast を使います。ルールはあえて短くしてあり、自分たちのマトリクスに合わせる必要があります。この関数は呼び出し先と calldata だけでなく、Safe トランザクションの 9 つのフィールドをすべて受け取ります。ガス代の払い戻しのフィールドでも資金が動くためです。

bash
# 誰かが署名する前に、エージェントが用意した下書きをマトリクスのどの行かに振り分ける。
# bash と Foundry の cast が必要。ルールは一例なので、自分たちのマトリクスに合わせて調整する。
SAFE=0x...                        # エージェントが下書きを用意する対象の Safe
TOKEN=0x...                       # このフローが動かせる唯一のトークンコントラクト
RECIPIENT=0x...                   # このフローで承認された唯一の受取先
TRANSFER=$(cast sig "transfer(address,uint256)")
ZERO=0x0000000000000000000000000000000000000000

classify() {                      # 引数: SafeTx の 9 つのフィールドを順番どおりに
  [ "$#" -eq 9 ] || { echo "STOP: pass all nine SafeTx fields"; return; }
  local to value=$2 data=${3,,} op=$4 selector=${3:0:10}
  to=$(cast to-check-sum-address "$1")
  if [ "$op" != "0" ]; then
    echo "ADMIN REVIEW: operation $op is not CALL; with 1, DELEGATECALL, the target code runs with the Safe's storage"
  elif [ "$to" = "$(cast to-check-sum-address "$SAFE")" ]; then
    echo "ADMIN REVIEW: call to the Safe itself, which can change owners, threshold, modules, guard or fallback handler"
  elif [ "$5 $6 $7" != "0 0 0" ] || [ "$(cast to-check-sum-address "$8")" != "$ZERO" ] || [ "$(cast to-check-sum-address "$9")" != "$ZERO" ]; then
    echo "STOP: gas refund fields are set; the Safe would pay a refund to refundReceiver"
  elif [ "$value" = "0" ] && [ "$to" = "$(cast to-check-sum-address "$TOKEN")" ] && [ "${selector,,}" = "$TRANSFER" ] && [ "${#data}" -eq 138 ] \
    && [ "$(cast decode-calldata "transfer(address,uint256)" "$data" | head -n 1)" = "$(cast to-check-sum-address "$RECIPIENT")" ]; then
    echo "SIGNERS: token transfer to the approved recipient; review the exact payload and amount"
  else
    echo "STOP: target, function, recipient or value outside the matrix"
  fi
}

特に重要なのは、通常の送金として通してはいけないケースです。同じ payload を DELEGATECALL で送るもの、モジュールを有効化したりフォールバックハンドラーを変更したりする Safe への呼び出し、送金の代わりの approve、一覧にないコントラクトや受取先に向けた送金、そしてガス代の払い戻しのフィールドを設定した下書きです。最後のケースでは、Safe が送金とは別に第三者へ支払うことになります。

bash
MODULE=0x...                      # レビュー中のモジュールのアドレス
NOREFUND="0 0 0 $ZERO $ZERO"      # safeTxGas baseGas gasPrice gasToken refundReceiver
PAY=$(cast calldata "transfer(address,uint256)" $RECIPIENT 5000000000)
classify $TOKEN 0 $PAY 0 $NOREFUND                                                   # SIGNERS
classify $TOKEN 0 $PAY 1 $NOREFUND                                                   # ADMIN REVIEW
classify $SAFE 0 $(cast calldata "enableModule(address)" $MODULE) 0 $NOREFUND        # ADMIN REVIEW
classify $SAFE 0 $(cast calldata "setFallbackHandler(address)" $MODULE) 0 $NOREFUND  # ADMIN REVIEW
classify $TOKEN 0 $(cast calldata "approve(address,uint256)" $RECIPIENT 5000000000) 0 $NOREFUND  # STOP
classify $MODULE 0 $PAY 0 $NOREFUND                                                  # STOP
classify $TOKEN 0 $(cast calldata "transfer(address,uint256)" $MODULE 5000000000) 0 $NOREFUND    # STOP
classify $TOKEN 0 $PAY 0 0 100000 5000 $TOKEN $MODULE                                # STOP

二つ目のチェックはチェーンを見ます。連携を外したら、そのモジュールはもう有効であってはならず、マトリクスに名前のないモジュール、ガード、フォールバックハンドラーも現れてはなりません。下の読み取りは Safe に設定されているものを一覧にし、そのあと外したモジュールのアドレスからの呼び出しをシミュレーションします。何も送信しません。

bash
export ETH_RPC_URL=...            # Safe のネットワークの RPC
cast call $SAFE "VERSION()(string)"
# 想定しているものだけでなく、有効なすべてのモジュール (最初の 10 件。2 番目の値が 0x...01 でなければ続きを取得)。
cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" 0x0000000000000000000000000000000000000001 10
# トランザクションガードとフォールバックハンドラーをストレージスロットから読む (ゼロなら未設定)。
cast storage $SAFE 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8
cast storage $SAFE 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5
# Safe 1.5.0 のみ: モジュールガード (ゼロならモジュールの実行を確認するガードはない)。
cast storage $SAFE 0xb104e0b93118902c651344349b610029d694cfdec91c589c91ebafbcd0289947
cast call $SAFE "isModuleEnabled(address)(bool)" $MODULE
# ネガティブケース: 有効化されたモジュールではないアドレスは、Safe を通じて実行できない。
# モジュールが無効な間の期待結果: GS104 でリバートする。結果が返るなら実行できるということ。
cast call $SAFE "execTransactionFromModule(address,uint256,bytes,uint8)(bool)" \
  $SAFE 0 0x 0 --from $MODULE

三つのブロックを書かれたとおりに、メインネットの公開 Safe 二つ、バージョン 1.3.0 と 1.4.1 に対して実行し、さらにモジュールを有効化してから無効化したローカルフォークでも実行しました。分類はコメントと一致し、シミュレーションした呼び出しはモジュールが有効な間だけ true を返しました。Safe 1.4.1 では、モジュールの一覧に、単一アドレスのチェックでは見えなかった有効なモジュールが表示されました。フォークでは、ガス代の払い戻しのフィールドを設定した下書きは STOP に分類されました。それでも実行すると、同じ下書きにより、送金に加えて Safe の残高から払い戻しの受取先へ支払いが行われました。これらのチェックがカバーするのは自分たちで書いたルールであり、モジュールのコードや事業者のポリシーのレビューの代わりにはなりません。

変更ごとに担当者を置く

各行には担当者が必要で、マトリクスの変更はアカウントの変更と同じように扱う必要があります。レビューのきっかけにすべき出来事は三つあります。Safe の新しいバージョン。モジュールに対するガードのカバー範囲が 1.4.1 と 1.5.0 で異なり、モジュールガードは明示的に設定する必要があるためです。事業者のポリシー、またはそれを編集できる人の変更。そして新しいモジュール、ガード、フォールバックハンドラー、DELEGATECALL の呼び出し先です。

記録には、誰が各変更を承認したか、どのネガティブケースを実行し、結果はどうだったかを残します。その記録があるからこそ、チームはあとから、エージェントがなぜそれをできたのか、あるいはなぜできなかったのかを説明できます。

よくある質問

この権限マトリクスで、AIエージェントが決してしないことは何ですか?AIエージェントは署名、トランザクションの送信、鍵の保持、Safe の管理変更の実行をしません。行うのは読み取り、デコード、シミュレーション、下書きの作成だけです。

Safe でモジュールを有効化するのは誰ですか?Safe のオーナーが、しきい値分の署名があるトランザクションで、管理用チャネルを通じて行います。すでに有効なモジュールや、Safe をデプロイした setup の呼び出しでも有効化できます。これも、エージェントにアクセス権を与える前に有効なモジュールをすべて一覧にすべき理由の一つです。エージェントはモジュールを分析し、トランザクションを下書きできます。

マトリクスを成果物にする

マトリクスは作業用のドキュメントです。能力ごとに一行、明記されたリソース、担当者、ルールごとの適用ポイント、それを確かめるネガティブケースを含みます。AIエージェントが Safe に触れる前に、チームは各アクションに必要な承認と、エージェントが範囲を超えようとしたときにどのテストが失敗したかを示せるべきです。

自社の Safe の権限について相談したい場合は、お問い合わせページからご連絡ください。メッセージには、この記事をご覧になった旨と、想定している Safe またはフローをご記入ください。鍵や機密性の高い運用資料は含めないでください。

参考資料

2026年10月1日に参照したドキュメントです。出典は仕組みと設定を説明するものです。マトリクス、モジュールのシナリオ、分類ルールはこの記事による提案です。実行し出力を保存したもの:三つのコードブロック。Foundry の cast 1.8.3 で、メインネットの公開 Safe 二つとローカルフォークで実行し、ガス払い戻しのケースも含みます。あわせて、Safe 1.4.1 のローカルフォークでのガードとモジュールのテスト、新しい Safe 1.5.0 のローカルフォークでのモジュールガードのテスト。ドキュメントと照合し、実行していないもの:Turnkey と Fireblocks のポリシーエンジンに関する記述。

関連記事

この内容をローンチやワークフローに適用したいですか?

技術レビューを依頼し、最も安全な第一歩を整理しましょう。

関連記事