T27 · Account Abstraction
利用者が秘密鍵を直接持たなくても、安全に使えるか?
- 練習する能力
- BuilderProtocol Literacy
- 実践
- スマートアカウントをデプロイし、セッションキーとガス代の肩代わりを設定して、ユーザーのガス代ゼロの操作を一度完了する。
- AI Lab
- AI にソーシャルリカバリーの手順を設計させ、悪用される 3 つのケースを自分で書く。
現実の問題
軽いオンチェーンアプリを作りました。機能自体は三回クリックで使えます。
新規利用者の実際の流れはこうです。
1. wallet extensionを入れる
2. 12語のseed phraseを写し、順番を確認する
3. native tokenを買い、最初のGasを用意する
4. 開始を押し、署名popupで確認する
5. 次へ進み、また署名する
6. さらに次で、もう一度署名する100人の訪問から、wallet導入30人、seed保存15人、Gas保有5人、初回完了2人になります。
機能ではなく、オンチェーンのアカウントになる段階で離脱しています。
三か月後、完了した二人の一人が、端末変更でwalletを失い、メモのseedも見つからず、復旧を頼みます。
できないと答えるしかありません。
両方の原因は、EOAでは秘密鍵がアカウントの全てだからです。持てば全権、失えば何もありません。
ゲームだけ、少額だけ、所定手順で復旧という範囲がありません。
全か無かを分解できるでしょうか?
思考実験
二種類を比べます。
第一、一鍵だけの金庫です。
鍵の持ち主が全権を持ち、用途で分けられず、紛失届も出せません。なくせば永久に開かず、盗まれれば中身を失います。
資金を動かせる人は物理的事実です。
第二、会社の銀行口座です。
鍵一つでなく規程があります。
- 担当者は5,000元まで
- 財務責任者は50,000元まで
- 500,000元超は二人の承認
- 家賃と光熱費は定期払い
- 印章紛失は公告、異議期間、再発行金額別、用途別、複数人、紛失復旧を実現します。
理由は一つです。
金庫の権限は物理的事実、会社口座の権限は読み、変更し、監査できる規則です。
Account Abstractionは規則をコードにします。公開鍵由来の住所ではなく契約になり、この操作を認めるかを関数が答えます。
しかし規則がコードになると、新しい問題があります。
規則を変える権限は誰のものか?
鍵喪失に加えて、規則を変更される失敗が生まれます。
リスクは物理問題からガバナンス問題へ移り、後半ではそれを扱います。
あなたが決める
漏斗の改善を一つ選びます。
結果を観察する
比較します。
| 初回負担 | 紛失 | 単一点 | 日常 | 新しい責任 | |
|---|---|---|---|---|---|
| EOAと案内 | 高い | 復旧不可 | seed | 毎回署名 | なし |
| custody | 最小 | 運営に依頼 | 運営 | 意識不要 | 全資産保管 |
| smartとsession | 低い | 設計次第 | 契約規則 | session中は不要 | 安全とGas |
| 複数鍵 | 中 | 設計次第 | 可用性の問題 | 毎回協力 | 調整 |
第三・四列から次が分かります。
鍵を消すのではなく、一鍵の全権を複数鍵の異なる権限と復旧経路へ分解します。
権限という概念がアカウント層に生まれます。
同時に次も必要です。
分割した権限の境界は、新しい攻撃面です。
EOAの鍵に加え、コード、session範囲、代付規則、復旧、それらを変える人があります。
問題の場所を移すことで、監査、テスト、assertionが可能になります。 seed喪失は検証できませんが、範囲誤りはできます。
T32 と同じく、policy engineを契約へ移します。
モデルを作る
検証と実行を分ける
全設計の出発点です。
validate(op) → 操作を認めるか。規則を確認する
execute(op) → 検証を通った操作だけを行うEOAでは署名成功と実行が直結します。smartでは判断を自分のコードにできます。
一操作の全経路
利用者は直接取引でなく操作意図を署名します。
- 意図を署名
- 専用poolへ
- bundlerがまとめて送信
- EntryPointが配分
- accountが検証
- paymasterが支払判定
- accountが実行
EOAにはない三役です。
| 役 | 仕事 | 障害 |
|---|---|---|
| bundler | 意図を取引へまとめる | 停止するが偽造しない |
| EntryPoint | 配分と課金 | 全accountの共通依存 |
| paymaster | Gasを負担 | 広い規則で引き出され続ける |
前章と同じ層の区別です。bundlerは伝送層で、到着にだけ影響し、真実性には影響しません。 数が多いので安全という説明は、層を取り違えています。
accountの核心
重要部分です。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract SmartAccount {
address public owner; // 主鍵
uint256 public nonce; // replay防止
address public immutable entryPoint; // 唯一の入口
struct SessionKey {
uint48 validUntil; // 期限
uint128 spendLimit; // 累計上限(最小単位)
uint128 spent; // 使用済み
bool enabled;
}
// session鍵から範囲
mapping(address => SessionKey) public sessions;
// session鍵、target、selectorごとの許可
mapping(address => mapping(address => mapping(bytes4 => bool))) public allowed;
constructor(address ep, address o) {
entryPoint = ep;
owner = o;
}
modifier onlyEntryPoint() {
require(msg.sender == entryPoint, "not entry point");
_;
}
/// 検証は判断だけ。外部呼出をしない
function validate(
bytes32 opHash,
bytes calldata signature,
address target,
bytes4 selector,
uint256 value
) external onlyEntryPoint returns (bool) {
address signer = recover(opHash, signature);
// 主鍵は無制限
if (signer == owner) return true;
// sessionの範囲を一項ずつ確認
SessionKey storage s = sessions[signer];
if (!s.enabled) return false;
if (block.timestamp > s.validUntil) return false;
if (!allowed[signer][target][selector]) return false;
if (value + s.spent > s.spendLimit) return false;
// 検証時に累計を消費し、同ブロックの重複利用を防ぐ
s.spent = uint128(s.spent + value);
return true;
}
/// 検証済みと入口が判断した後に実行
function execute(address target, uint256 value, bytes calldata data)
external
onlyEntryPoint
{
(bool ok, bytes memory ret) = target.call{value: value}(data);
if (!ok) {
assembly { revert(add(ret, 32), mload(ret)) }
}
}
function recover(bytes32, bytes calldata) internal pure returns (address) {
return address(0); // frameworkの署名復元に置換
}
}四つの判断が事故に対応します。
第一、onlyEntryPoint。 制限がなければ誰でもexecuteで送金できます。T7 の公開入口の原則です。
第二、累計を検証時に引きます。 後なら同ブロックでspent旧値を繰り返し使えます。使用後の更新というreentrancyの形です。
第三、検証は外部呼出しません。 offchain simulationと実状態の差で失敗すると、bundlerが無駄なGasを払い、繰り返せばサービスしなくなります。
第四、実行失敗の元errorをそのまま返します。 T14 の複数動作に一 status しかない問題は、内側のrevert dataを変更せず上へ伝えることで解きます。省くとUIは取引失敗だけを表示し、どの段階か永遠に説明できません。
session:範囲が全て
弱い鍵ではなく、範囲付きの鍵です。
六項目が必要です。
| 次元 | 省くと |
|---|---|
| 期限 | 漏えいが永久有効 |
| target | 任意契約へ |
| selector | approveも可能 |
| 一回上限 | 一度で空にする |
| 累計 | 小分けで回避 |
| 再授権 | 自分で別sessionを作る |
第三行を詳しく見ます。対象契約だけを制限し関数を制限しないと、ゲームだけのsession keyがゲーム契約の承認関数を呼び、攻撃者管理のaddressへ枠を渡して、後で外から徐々に引き出せます。
T32 の授権経由の例と同じです。
送金だけでなく、資金の制御権を変える全操作をpolicyで覆います。
代付:支払側もpolicyが必要
無制限のスポンサーは継続して消費できるpoolです。
三方式です。
| 方式 | 負担者 | リスク |
|---|---|---|
| 全額スポンサー | project | 無意味な高Gas |
| 別token払い | user | oracle操作、transfer hook |
| 初回N回だけ | project、上限あり | 一人の多数account |
回数、額、日予算、target allowlist、即時停止が必要です。
表の第二行のtransfer hookは T9 の話です。非標準tokenの transfer は外部へ実行権を渡し、受取時に注意しないPaymasterの核心経路へ外部callを持ち込みます。次章でその結果を完全に実験します。
復旧:最も危険な節
基本手順です。
1. N guardian、threshold Mを事前設定
2. M署名で主鍵変更を提案
3. timelock T日
4. 現主鍵は期間中いつでも取消
5. 取消なしなら有効化待機と取消がなければguardianは乗っ取り鍵です。
紛失だけでなく悪用も考えます。
| 悪用 | 内容 | timelock |
|---|---|---|
| 共謀 | thresholdで奪う | 本人の取消が必要 |
| 非独立 | 三住所が同一人 | 防げない |
| 社会工学 | 本人を装う | 本人の取消頼み |
| 本人不在 | 入院、旅行、死亡など | 構造的盲点 |
| guardian紛失 | Mへ届かない | 可用性 |
| owner逆悪用 | 譲渡約束後に取り戻す | ガバナンス |
第四行は重要です。timelockは、期間中に利用者が通知を見て取消できることが全前提です。鍵を失う原因が正常なnetwork利用もできなくする出来事なら、最も復旧を必要とする場面でこの前提は成立しません。
三つが重要です。
- eventとoffchain通知を出します。 見えない提案には待機の意味がありません。
- 取消は無条件・即時です。 追加要件を付けません。
- guardian独立性を確認します。 方法と確認日を残します。
timelockの長さは技術定数でなく、製品上のトレードオフです。 短ければ共謀を防げず、長ければ正当な復旧者を待たせます。accountの金額によって期間を変えます。T17 の業務risk別確認深度と同じ発想です。
何と呼ぶか
署名を認めるかをコードが決める契約accountです。
EOAの暗号学的事実を、読めて検証できる規則へ変えます。
target、data、value、Gas上限、paymasterを持つ構造化意図です。
送信者とGas支払者を分けられます。
意図をまとめて取引として送ります。
止められても偽造はできません。 事前検証をsimulateするためvalidateが制限されます。
配分・課金の共通入口です。
失敗すると全accountに及ぶため厳格な監査が必要です。
スポンサーや別tokenでGasを払います。
いつ拒否するかを決めなければ、誰でも使えるATMです。
期限、target、selector、一回・累計上限を持つ一時鍵です。
範囲が全てです。 targetだけでは授権で額を回避できます。
事前guardianで主鍵を替えます。
成立にはtimelock、chain上eventとchain外通知、現主鍵の無条件取消権の三つが必要です。一つでも欠けると、guardianは復旧の協力者からaccountを奪える人へ変わります。
実践
localかtestnetだけです。 意図的な欠陥を作り修正します。
四つのred/green基準です。
1. 主鍵は任意操作
2. sessionは範囲外をrevert
3. native残高ゼロでも成功
4. 授権で額上限を回避できない主鍵だけの最小accountを作ります。
SmartAccountをowner、nonce、validate、executeへ簡略化します。
最初は負のテストで、無関係な住所からexecuteしてrevertを要求します。
機能より先に書きます。 第一の要件はアクセス制御です。
EntryPointで全経路を通します。
意図作成、署名、bundler、inclusionを観察します。
三問です。
- 取引fromは誰か?
- Gasはどこから引くか?
- validateは何回呼ばれたか?利用者addressは取引の送信者fieldへ現れなくなります。 これが最初の問いから分かる大きな変化です。indexer、risk判定、送信者でfilterするqueryの全てを合わせて変更します。
targetだけを制限したsessionを追加します。
他contractの呼出が拒否されることを確認します。
重要な攻撃実験として、許可されたアプリの授権関数で自分の別addressへ権限を渡し、引き出します。
selectorなしでは額上限を回避できます。
実際に成功させて意味を理解します。
selectorと累計を加え、攻撃を失敗させます。
修正前は攻撃成功、修正後は拒否となる同じ試験を使います。
コンパイルだけでは修正確認になりません。
累計を引く時点を確認します。
一取引で繰り返し、小額合計を上限以上にします。
spentが後更新なら攻撃が成功します。validate時へ移して拒否します。
使用後に更新する形を覚えます。
代付でGasゼロにします。
まず無制限でゼロ残高の成功を確かめます。
すぐ回数、日予算、target、停止を追加します。
高Gasの連続無意味操作を拒否する試験を行います。
復旧を作って自分で攻撃します。
guardian、threshold、待機、取消を実装し、三試験を行います。
- 期間内の取消で無効化
- 取消なしなら有効化
- eventに提案者、新owner、有効時刻最後は本人が乗っ取りを知れるかの根拠です。
権限台帳を作ります。
| 誰 | 操作 | 上限 | 期限 | 取消 |
|---|---|---|---|---|
| 主鍵 | 任意 | なし | 永続 | 復旧 |
| session | allowlist関数 | 一回・累計 | expiry | 主鍵即時 |
| guardian | owner変更提案 | 変更だけ | 常駐 | 主鍵期間内 |
| paymaster | Gas | 日予算 | 常駐 | 緊急停止 |
コード根拠を示します。空欄は未設計の権限です。
AI Lab
第三段階が本題です。
第一:復旧の全状態、遷移event、各役の操作をcontract関数単位で設計してください。
第二:「___の不正・失敗でaccountが___される」と、役割と人数で仮定を列挙してください。
第三:奪取を狙う攻撃者として六経路以上を示し、
前提、手順、防げない段階、本人の発見可能性を記してください。
社会工学と時機も含めます。第一は標準解のある問題で、待機と奇妙な取消条件を見ます。
第二段階で理解を確かめます。「guardianが不正なら奪われる」は、正しくても情報量がありません。何人、どの時間窓、ownerはそのとき何をしているかを追加して初めて、使える信頼仮定になります。
第三段階がLabの価値で、経路の列挙はモデルの得意分野です。出張中に開始、偽の安全upgrade通知で本人に承認させる、通知channelを先に侵害して復旧、guardianの端末交換まで潜伏して入り込むなど、具体的な案を示します。
自分で二つ加えます。
第一、guardian非独立。五人に見えて三住所が同一人ならthresholdが崩れます。
第二、ownerの逆悪用。譲渡済みaccountの奪還は共同管理で実在するガバナンスリスクです。
最後は自分で三つの悪用を動くテストにします。説明だけの攻撃と成功する攻撃テストでは、信頼度が一桁違います。T8 からの同じ基準です。
AI の回答後、自分で必ず検証すること
- timelockがあるか
- 主鍵の取消が無条件・即時か
- eventと通知を届けるか
- 同一人の複数guardianを考慮したか
- 本人不在という盲点を認めたか
- guardian鍵喪失でthreshold不足を考えたか
- 待機期間を金額に応じて設計したか
- 三悪用を実行可能テストへする
実例
多くのsmart accountは、先に最小proxyを公開し、利用者が後からowner設定の初期化を呼びます。callerを制限しない初期化では、その間に誰でも自分をownerにできます。
大量部署では数分の公開窓になります。
T8 の2017年libraryと同じ、無防備な初期化の形です。 deploymentと初期化を一取引にまとめ、初期化関数にも一回だけという確認を入れます。
全利用者へ無制限でGasを提供しました。
scriptが無意味な高Gas操作を反復し、数時間で空になりました。
盗むのでなく、運営へ焼かせた事故です。 回数、額、日予算、targetで防ぎます。
他人への支払いには拒否規則が必要です。
ゲーム限定のsessionに小額上限がありました。
攻撃者は送金でなく授権関数で大allowanceを与え、別住所で引き出しました。
approveのvalueはゼロなので制限に触れません。
T32 と同じ送金だけの上限では制御権変更を防げません。
同じaddressを複数chainに置き、chain IDを署名へ入れませんでした。
Aの署名をBで再利用できました。
前章のクロスチェーンmessage IDへ宛先chainを含める規則と同じです。検証する署名は、chain、契約address、nonceの適用範囲へ全て結び付けます。
変数を一つ変える
月間の再授権を省けます。
漏えい後の露出期間は720倍になります。その間にアプリがupgradeして、元の権限範囲で考えていない関数を追加する可能性があり、利用者も何を許可したか忘れているという、三軸のriskが増えます。
累計と狭いselectorで補い、残り期間と範囲を見せます。
期限と範囲は釣り合いが必要です。
共謀と社会工学は減ります。
可用性riskは増えます。同じ家の三端末は、一火災、引っ越し、盗難で全て失い得ます。 guardianが同時に失敗しないというsocial recoveryの仮定を、同じ場所の端末が破ります。
同じ引き出しに三鍵を置いた金庫へ戻ります。
修正、機能、新標準へ資産移動なしで対応できます。
accountに全規則を越える権限を加えます。実装をupgradeできる人は、全検証ロジックを一度に変えられます。 session範囲、復旧timelock、Paymasterのallowlistも全て無効にできます。
T8 の三原則を全て守り、upgrade権限はアプリ開発者でなくaccount owner自身へ置きます。 開発者が持つと、利用者資産の安全性が開発者の秘密鍵に依存し、明示していないcustodyへ戻ります。
持ち帰る問い
誰が利用者かが技術選択を決めます。crypto経験者ならEOAと良い案内で十分かもしれません。秘密鍵を初めて聞く人にはseedを書き写すことが壁です。新技術のためでなく、その具体的な人のために選びます。
誰が払うか?送信者とGas支払者を分けると、Paymasterの財源、拒否条件、枯渇後も製品を使えるかを改めて答える必要があります。答えのないスポンサー案は、公開後の運用事故になります。
期限、target、selector、一回、累計、再授権の六項目を契約へ書きます。 T32 のoffchain policyと両方使います。
章末チェック
署名を認める条件です。
EOAでは対応する秘密鍵の署名が有効という暗号的事実で、中間状態も条件もありません。smart accountでは、時間、額、対象、関数、call数、複数人確認を調べるコードへ変わります。
暗号事実からプログラム可能なpolicyへ変えます。
誤ったpolicyには責任があります。
大きく不足します。
対象だけなら、その契約の全関数、承認関数も呼べます。攻撃者addressへ枠を承認すると、後の送金は元accountを通らず、金額制限も起動しません。承認自体のvalueがゼロだからです。
制御権を変える全操作、委任、admin変更、upgradeまで含めます。
bundlerが事前simulateするからです。
外部状態へ依存すると、chain外simulationは通ってもchain上実行が失敗し得ます。取引を送ったbundlerがGasを失い、極小費用で数百回繰り返すだけで、そのaccountのサービスを拒否させられます。
DoS防止設計です。
共謀を防ぐには本人が見て取消する必要があります。
防げないもの:
- 本人不在という構造的盲点。
- guardian非独立。
- 通知侵害。
eventと通知、即時無条件取消と組み合わせます。
確認項目:
- コード根拠があるか?
- 六次元が揃うか?
- paymasterの拒否はどこで実装するか?
- 復旧eventと通知は?
- upgradeは誰か、timelockは?
- 制御権変更をすべて覆うか?
- どの鍵が何をしたかeventで復元できるか?
開発者がupgradeを持てばcustodianです。
要点
アカウント抽象化は「誰が署名できるか」をプログラム可能なポリシーに変える。