T27 · Account Abstraction
能不能让用户不直接持有私钥,也依然安全?
- 练习的能力
- BuilderProtocol Literacy
- 动手
- 部署一个智能账户,配置会话密钥与代付,完成一次用户零 Gas 的操作。
- AI Lab
- 让 AI 设计社交恢复流程,自己写出三种它会被滥用的场景。
一个现实问题
你做了一个链上应用,功能很轻,第一次使用只要点三下。
新用户的实际流程是这样的:
1. 装一个钱包扩展
2. 抄下 12 个英文单词,抄完还要按顺序选一遍
3. 去某个地方买一点原生代币,否则连第一笔操作都发不出去
4. 点「开始」,弹出签名框,确认
5. 点下一步,又弹一次
6. 再下一步,再弹一次你去看漏斗:一百个人点进来,装完钱包的三十个,抄完助记词的十五个,账户里有 Gas 的五个,完成第一次操作的两个。
产品本身没有任何问题。流失全部发生在「成为一个链上账户」这件事上。
更难受的是另一半。三个月后,那两个完成操作的用户里,有一个发消息问你:换手机之后钱包没了,助记词当时抄在一张便签上,找不到了,能不能帮忙找回。
你只能回答不能。
把这两件事放在一起看,会发现它们是同一件事的两面:在 EOA 的世界里,私钥就是账户的全部。 有它就有全部权限,没它就什么都没有。
没有「只能玩这个游戏」的权限,没有「单笔不超过十块钱」的权限,没有「丢了可以按流程恢复」的路径。
所以问题是:这个「全有或全无」,能不能拆开?
思想实验
把两种账户放在一起对比。
第一种:一个只有一把钥匙的保险柜。
钥匙在谁手里,柜子就归谁。钥匙不区分用途——它不能只打开柜子的左半边。钥匙也不能挂失——柜子不认人,只认钥匙。丢了就是永远打不开,被偷了就是东西没了。
在这个世界里,「谁能动这些钱」是一个物理事实。
第二种:一家公司的对公账户。
这里没有「一把钥匙」,只有一本章程:
- 出纳可以付 5000 元以下
- 财务总监可以付 5 万元以下
- 超过 50 万,需要两个人签字
- 每月的房租和水电是定期付款,不用每次审批
- 如果董事长的印章丢了,按流程公示、等待异议期、重新刻一个注意这本章程做到了保险柜做不到的四件事:按金额分级、按用途授权、多人共同决定、以及钥匙丢了有救。
而它能做到这些,只因为一个差别:
在保险柜里,「谁能动钱」是物理事实;在公司账户里,「谁能动钱」是一段可以被阅读、被修改、被审计的规则。
账户抽象要做的,就是把这段规则搬到链上,写成代码。账户不再是一个公钥算出来的地址,而是一份合约,合约里有一个函数专门回答一句话:「这笔操作,我认不认。」
到这里故事很美好。但推演不能停在这里,因为规则一旦变成代码,一个新问题就出现了:
改这段规则的权力,归谁?
保险柜的世界里只有一种死法:钥匙丢了。规则的世界里多了一种:规则被改了。
你没有消灭风险,你把它从一个物理问题变成了一个治理问题。这一章后半段的全部内容,都在处理这个新问题。
你来决定
回到那个漏斗。你要改善它,只能先做一件事。
观察结果
四条路的对照:
| 首次使用的门槛 | 密钥丢了怎么办 | 单点在哪 | 日常体验 | 你新增的责任 | |
|---|---|---|---|---|---|
| EOA + 好引导 | 高 | 没救 | 用户的助记词 | 每步都签 | 无 |
| 托管 | 最低 | 找你 | 你 | 无感 | 保管全部用户资产 |
| 智能账户 + 会话密钥 | 低 | 看恢复怎么设计 | 账户合约的规则 | 会话期内无感 | 合约安全 + Gas 账单 |
| 多把密钥共同决定 | 中 | 看恢复怎么设计 | 无单点,但有可用性风险 | 每次需多方 | 协调与可用性 |
第三列和第四列放在一起看,第一条结论就出来了:
账户抽象没有消灭密钥,它把「一把密钥的全部权限」拆成了「多把密钥的不同权限」,外加一条密钥丢失之后的恢复路径。
这是一次实打实的改进——它让「权限」这个词第一次在账户层面有了意义。
但也正因为如此,第二条结论必须同时说出来:
权限一旦可以被拆分,权限边界就成了新的攻击面。
EOA 的攻击面只有一个:私钥。智能账户的攻击面至少有五个:账户合约本身的代码、会话密钥的范围定义、代付方的赞助规则、恢复机制、以及谁能改上面这四样东西。
这不是在说账户抽象不好,而是在说它把问题换了个位置。换过去的那个位置有一个巨大的好处:它是可以被审计、被测试、被写成断言的。 助记词丢了没有测试可以写,会话密钥的范围写错了有。
你会发现这套东西和 T32 的结构一模一样:结构化的提案、确定性的校验、可配置的策略、可追溯的日志。账户抽象就是把那套策略引擎搬进了合约里。
建立模型
一条核心分割线:验证与执行分离
智能账户的全部设计,都从这一条分割线展开:
validate(op) → 这笔操作我认不认?(只看规则,不做事)
execute(op) → 做事(只在 validate 通过之后)EOA 里这两件事是同一件事:签名对了就执行,没有中间状态。智能账户把它们拆开,于是「认不认」变成了一段你可以自己写的代码。
一笔操作的完整流转
用户不再直接发交易,而是发一份操作意图,由别人打包上链。
- 用户签一份操作意图
- 进入专用的待处理池
- 打包者挑出来组成一笔交易
- 入口合约逐个分发
- 账户合约验证
- 代付方决定付不付
- 账户合约执行
这条链上有三个角色是 EOA 世界里不存在的:
| 角色 | 做什么 | 它出问题会怎样 |
|---|---|---|
| 打包者 | 把多份意图组成一笔真正的交易发上链 | 只影响活性:没人打包,操作卡住,但伪造不了 |
| 入口合约 | 统一的分发与计费入口 | 它是所有账户的共同依赖,必须被极其严格地审计 |
| 代付方 | 替用户出 Gas | 它的赞助规则写宽了,就是一个可以被持续提取的池子 |
第一行和上一章的分层是同一个道理:打包者属于传输层,它只影响消息送不送得到,不影响消息真不真。 说「我们有很多打包者所以很安全」是一句错位的话。
账户合约的核心
把最关键的部分写出来,剩下的都是外围:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract SmartAccount {
address public owner; // 主密钥
uint256 public nonce; // 防重放
address public immutable entryPoint; // 唯一允许调用的入口
struct SessionKey {
uint48 validUntil; // 到期时间
uint128 spendLimit; // 累计额度上限(最小单位)
uint128 spent; // 已用额度
bool enabled;
}
// 会话密钥 → 授权范围
mapping(address => SessionKey) public sessions;
// 会话密钥 → 目标合约 → 函数选择器 → 允许与否
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;
// 会话密钥:逐项检查范围
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); // 用你的框架里的签名恢复实现替换
}
}这段代码里有四个决定,每一个都对应一类真实事故。
第一,onlyEntryPoint。 账户合约必须限定调用来源,否则任何人都能直接调 execute 把钱转走。这是 T7 那条「对外入口必须假设任何人都会调它」的直接应用。
第二,累计额度在验证阶段就扣。 如果放到执行之后再扣,同一把会话密钥可以在同一个区块里被用很多次,每次检查时 spent 都还是旧值。这就是下一章重入的雏形:状态更新晚于使用,就会被重复利用。
第三,验证阶段不做外部调用。 这一条看起来是洁癖,实际上是打包者的生命线:打包者在链下先模拟一遍验证,通过了才打包上链;如果验证依赖外部状态,链下模拟通过、链上执行失败,打包者白付 Gas。成百上千次之后它就不再服务你的账户了。所以验证阶段的可用操作是被严格限制的。
第四,执行失败时把原始错误原样抛出。 T14 讲错误映射时留了一个尾巴:一笔交易里打包了多个动作,回执只有一个 status。这里是它的解法——账户合约必须把内层的 revert 数据原样冒泡出来,否则前端只能显示「交易失败」,永远说不清是哪一步失败的。
会话密钥:范围就是全部
会话密钥不是「一把弱一点的密钥」,而是一把带范围的密钥。范围写得对不对,决定了它是一个功能还是一个漏洞。
一份可用的范围定义至少包含六项:
| 维度 | 不写会怎样 |
|---|---|
| 有效期 | 一把泄露的密钥永远有效 |
| 允许的目标合约 | 能调任何合约,包括代币合约 |
| 允许的函数选择器 | 能调目标合约的任何函数,包括 approve |
| 单笔上限 | 一次就能把账户清空 |
| 累计上限 | 分成一百笔小额绕过单笔上限 |
| 能否再授权 | 会话密钥自己又签出一把新的会话密钥 |
第三行要单独说。很多实现只限制了「能调哪个合约」,没限制「能调哪个函数」。于是一把只该用来玩游戏的密钥,可以对游戏合约调用一次授权类的函数,把额度授给一个攻击者控制的地址,然后在链下慢慢取走。
T32 里那个「通过授权而不是转账」的案例,在这里原封不动地重演一次。它的教训也一样:
策略必须覆盖所有会改变资金控制权的动作,而不只是转账。
代付:谁付 Gas,谁就要有自己的策略
代付方是一个替别人付钱的合约。这句话本身就说明了它的风险:任何没有边界的赞助,都是一个可以被持续提取的池子。
三种常见模式:
| 模式 | 谁最终承担 | 主要风险 |
|---|---|---|
| 项目方全额赞助 | 项目方 | 被构造高 Gas 的无意义操作薅干 |
| 用户用其他代币付 | 用户 | 价格来源被操纵,或代币本身有转账钩子 |
| 限额赞助(新用户前 N 次) | 项目方,有上限 | 女巫攻击:一个人造一万个账户领一万份 |
代付方自己的策略至少要有:每个账户的次数与金额上限、全局的日预算、允许被调用的目标合约白名单、以及一个能立刻关掉的开关。
第二行那个「代币本身有转账钩子」是 T9 埋下的线:非标准代币的 transfer 可能把执行权交给别人。代付方在收款时如果不注意这一点,会在自己的核心路径上引入一次外部调用。下一章会把这件事的后果完整演示一遍。
恢复:这一节是这一章最危险的部分
社交恢复的标准流程:
1. 账户主人预先指定 N 个守护人,设定阈值 M
2. 主密钥丢失时,M 个守护人签名,发起一次「把主密钥换成新地址」的提议
3. 进入时间锁,等待 T 天
4. T 天之内,当前主密钥可以一键取消这次提议
5. T 天之后没有被取消,恢复生效第 3 步和第 4 步是整个机制的安全性来源,没有它们,M 个守护人等于 M 把可以直接夺取账户的钥匙。
但即便有它们,这套机制仍然有大量被滥用的空间。把它们列全很重要,因为设计恢复流程时最容易犯的错,就是只想到「用户丢了密钥」这一种情况:
| 滥用方式 | 发生了什么 | 时间锁能不能挡住 |
|---|---|---|
| 守护人合谋 | 够数的守护人一起发起恢复,接管账户 | 只能挡住一半:用户必须在窗口内看到并取消 |
| 守护人不独立 | 用户以为选了 5 个朋友,其中 3 个是同一个人的 3 个地址 | 挡不住,阈值形同虚设 |
| 社工守护人 | 攻击者冒充用户逐个联系守护人「我手机丢了」 | 同上,只能靠用户自己取消 |
| 用户失联 | 出差、住院、服刑、去世,窗口期内没人取消 | 挡不住,这是时间锁的结构性盲区 |
| 守护人自己丢钥匙 | 需要恢复时凑不够 M 个,恢复机制本身失效 | 无关,这是可用性失效 |
| 主人反向滥用 | 账户主人用恢复机制夺回一个已经承诺给他人的账户 | 无关,这是治理问题 |
第四行值得反复强调。时间锁的全部前提是用户在窗口内能看到通知并且有能力取消。这个前提在最需要恢复机制的那些场景里恰恰不成立——一个人之所以丢了密钥,往往就是因为发生了让他没法正常上网的事。
所以设计恢复流程时,有三条比「选几个守护人」重要得多:
- 恢复提议必须产生一个链上事件,并且要有链下通知的通道。 用户看不见的提议,等于没有时间锁。
- 当前主密钥的取消权必须是无条件、无延迟的。 取消这个动作本身不能再有任何门槛。
- 守护人的独立性要被验证,而不是被假设。 至少记录每个守护人是怎么被确认的、什么时候确认的。
还有一条属于产品而不属于代码:时间锁的长度是一次权衡,不是一个技术参数。 短了防不住合谋,长了让真正需要恢复的人等太久。不同金额的账户应该给不同的长度,这和 T17 那张按业务风险分档的确认深度表是同一个思路。
它叫什么
账户本身是一份合约,「谁能签名」由合约里的一段代码回答。
它和 EOA 的根本区别不是功能多少,而是:EOA 的权限是密码学事实,智能账户的权限是一段可被阅读、测试和修改的规则。
用户签署的不是一笔交易,而是一份「我想做什么」的结构化对象:目标、数据、金额、Gas 上限、代付方。
它由别人打包上链,所以发起交易的人和支付 Gas 的人可以是不同的人。这一条是零 Gas 体验的全部来源。
把多份操作意图组装成一笔真正的交易并发上链的角色。
它属于传输层:没有它操作会卡住,但它伪造不了任何东西。 它会先在链下模拟验证阶段,模拟不过就不打包——这就是验证阶段被严格限制的原因。
所有智能账户共用的分发与计费入口。账户合约只信任来自它的调用。
它是这套体系里最大的共同依赖:一旦它出问题,所有账户一起出问题,所以它的代码必须是整条链上被审计得最彻底的合约之一。
替用户支付 Gas 的合约。可以是项目方赞助,也可以是用户用其他代币折算支付。
它的核心设计不是「怎么付」,而是「什么情况下不付」。没有拒绝规则的代付方,就是一个对所有人开放的取款机。
一把被限定了范围的临时密钥:有效期、目标合约、函数选择器、单笔上限、累计上限。
范围定义就是它的全部。 只限制目标合约不限制函数,是这一类实现最常见的漏洞——一次授权类调用就能绕过所有金额限制。
主密钥丢失时,由若干个预先指定的守护人共同发起替换的机制。
它必须同时具备三件事才成立:时间锁、链上事件加链下通知、当前主密钥的无条件取消权。缺任何一件,守护人就从「恢复的帮手」变成了「可以夺取账户的人」。
动手
全程本地网络或测试网。 这个 Lab 会故意留出漏洞再补上,任何一步都不该碰真钱。
验收标准四条,每一条都是一个会红会绿的测试:
验收 1:主密钥能执行任意操作
验收 2:会话密钥只能执行范围内的操作,范围外必须 revert
验收 3:用户账户里一分钱原生代币都没有,操作仍然成功
验收 4:会话密钥无法通过授权类调用绕过额度限制先写一个最小账户,只有主密钥。
把本章那段 SmartAccount 精简到只剩 owner、nonce、validate、execute,部署到本地网络。
第一个测试就写负面用例:用一个随机地址直接调 execute,必须 revert。
先写这个测试,再写功能。 账户合约的第一条命是访问控制,不是功能。
接入入口合约,走一次完整流程。
用你的框架里对应的做法构造一份操作意图,签名,交给打包者,观察它落链。
重点看三件事:
- 链上那笔交易的 from 是谁?(不是用户)
- Gas 是从哪个账户扣的?
- 你的 validate 被调用了几次?第一个问题的答案会让很多人第一次真正理解这套体系:用户的地址从此不再出现在交易的发起方字段里。 你的索引器、你的风控、你的「按发起方过滤」的查询,全都要跟着改。
加会话密钥,先只限制目标合约。
授予一把会话密钥,只允许它调用你的应用合约。写测试验证:调用别的合约会 revert。
然后做这个 Lab 最重要的一次攻击练习:用这把会话密钥,对你的应用合约调用一个授权类的函数,把一笔额度授给另一个你控制的地址,再用那个地址把钱取走。
它会成功。范围里没有函数选择器这一项,额度限制就是纸糊的。
亲手做成一次,比读十遍「要限制函数选择器」有用得多。
补上函数选择器白名单和累计额度,把上一步的攻击测试跑成红色。
这是这个 Lab 的核心验收:同一个测试,在补漏之前必须通过(攻击成功),补漏之后必须失败(攻击被拦)。
编译通过不算修好,测试从绿变红才算。
验证累计额度的扣减时机。
写一个测试:在一笔交易里让同一把会话密钥连续执行多次小额操作,总额超过累计上限。
如果 spent 是在执行之后才更新的,这个测试会通过,也就是攻击成功。把它改成验证阶段就扣,测试变红。
记住这个形状:状态更新晚于使用。下一章会用同一个形状把一个合约的钱全部取走。
加代付方,实现零 Gas。
写一个最简单的代付方,先不加任何限制,让用户在账户余额为零的情况下完成一次操作。
然后立刻加限制:单账户次数上限、全局日预算、目标合约白名单、一个紧急开关。
加完之后写一个「薅羊毛」测试:用一个账户连续发起大量高 Gas 的无意义操作,验证它在达到上限后被拒绝。
加社交恢复,然后自己攻击它。
实现守护人、阈值、时间锁、取消。跑通正常恢复流程之后,写三个测试:
- 够数的守护人发起恢复,主密钥在窗口内取消:恢复必须失效
- 够数的守护人发起恢复,窗口内无人取消:恢复必须生效
- 恢复提议必须发出一个链上事件,事件里带发起人、新主密钥、生效时间第三条不是功能,是用户能不能知道自己正在被夺取账户的唯一依据。没有它,时间锁只是一段没人看的等待。
最后做一次账本。 把这个账户的全部权限列成一张表:
| 谁 | 能做什么 | 上限 | 有效期 | 谁能撤销 |
|---|---|---|---|---|
| 主密钥 | 任意操作 | 无 | 永久 | 只有恢复流程 |
| 会话密钥 | 白名单里的函数 | 单笔与累计 | 到期自动失效 | 主密钥,立即 |
| 守护人集合 | 发起恢复提议 | 只能换主密钥 | 常驻 | 主密钥,窗口期内 |
| 代付方 | 替这个账户出 Gas | 日预算 | 常驻 | 紧急开关 |
每一行都要能从代码里找到出处。填不满的格子,就是你还没想清楚的权限。
AI Lab
分三步,第三步是真正的考题。
第一步:
为一个智能账户设计社交恢复流程。
给出完整的状态机:有哪些状态、什么事件触发状态转移、
每个状态下谁能做什么。用合约函数的粒度描述,不要用抽象说法。
第二步:
列出这套流程的全部信任假设,每一条写成
「如果 ___ 作恶或失效,账户会被 ___」。
主语要具体到角色和数量。
第三步:
现在扮演攻击者。你的目标是夺取一个使用了这套流程的账户。
给出至少 6 条攻击路径,每一条包含:
- 你需要什么前提条件
- 具体的操作步骤
- 这套流程的哪一个环节没有挡住你
- 账户主人有没有机会发现
不要只考虑技术手段,社会工程和时机选择也算。第一步模型通常给得很完整,因为社交恢复是一道有标准答案的题。要盯的只有两处:有没有时间锁,以及主密钥的取消权有没有被加上奇怪的前置条件。
第二步会暴露它是不是真的理解了。很多回答会写「如果守护人作恶,账户会被夺取」——这句话是对的,但没有信息量。追问下去:几个守护人?在什么时间窗口内?账户主人此时在做什么?把这三个变量填进去,才是一条可用的信任假设。
第三步是这个 Lab 的价值所在,而且模型在这一问上表现相当好——枚举攻击路径本来就是它擅长的任务。常见的产出包括:挑用户出差时发起、伪造一个「安全升级」的通知让用户自己批准、先攻破通知渠道再发起恢复、长期潜伏等到某个守护人换设备时补位。
但有两条它很少主动提出,需要你自己补上:
第一,守护人不独立。 用户以为自己选了五个朋友,实际上其中三个是同一个人的三个地址。阈值在这种情况下完全失效,而链上看不出任何异常。
第二,恢复机制的反向滥用。 账户主人自己用恢复流程,去夺回一个他已经承诺交给别人的账户。这不是安全漏洞,是治理漏洞——而它在多人共管的账户里是真实存在的风险。
最后那条验证必须你自己做:把三种滥用场景写成会跑的测试。 一个描述出来的攻击和一个能跑绿的攻击测试,可信度差一个数量级。这一点从 T8 起就是同一条标准。
AI 说完之后,你必须自己验证
- 它的方案里有没有时间锁:没有时间锁的社交恢复,等于把账户直接交给守护人集合
- 当前主密钥能不能无条件、无延迟地取消一次恢复提议:有任何前置条件都要追问为什么
- 恢复提议有没有产生链上事件,以及链下通知怎么送达:用户看不见的提议等于没有时间锁
- 它有没有考虑守护人不独立的情况(同一个人的多个地址),以及怎么缓解
- 它有没有考虑用户在窗口期内失联:这是时间锁的结构性盲区,回避它的方案是不完整的
- 它有没有考虑守护人自己丢失密钥导致凑不够阈值:恢复机制自己也会失效
- 它给的时间锁长度是不是一个拍脑袋的常数,有没有按账户金额分档
- 最后自己写出三种滥用场景,每一种都要能在测试里复现,不能只是描述
真实案例
很多智能账户是「先部署一个最小代理,再由用户调用一次初始化设定主密钥」。如果初始化函数没有限定调用者,部署和初始化之间的那个窗口里,任何人都可以把自己设成主人。
批量部署账户的场景下,这个窗口可能长达几分钟,而且是公开可见的。
这和 T8 里那个 2017 年的库合约案例是同一个形状:初始化路径没有保护。 正确做法是把初始化和部署放进同一笔交易,并且在初始化函数里加上「只能被调一次」的检查。
一个项目方为了拉新,赞助所有用户的 Gas,没有设任何上限。
有人写了个脚本,不断发起消耗极高但毫无意义的操作,几个小时内把赞助池花光。所有正常用户在那之后都用不了。
这不是攻击者偷走了资产,而是他让项目方替他烧掉了资产。 防御手段很朴素:单账户次数与金额上限、全局日预算、只赞助白名单里的目标合约。
一条更普遍的教训:任何「替别人付费」的设计,都必须先写清什么情况下拒绝付费。
一把会话密钥被限定为「只能调用游戏合约」,单笔上限设得很小。
攻击者拿到这把密钥后,没有去转账——他调用了游戏合约上一个授权类的函数,把一个大额度授给了自己控制的地址,然后用那个地址把钱取走了。
整个过程中,金额上限一次都没有被触发,因为授权操作本身的 value 是零。
这是 T32 那个案例在账户层的重演:限额只管转账,攻击者就去改控制权。
一份智能账户合约被部署到多条链上,同一个用户在每条链上的地址相同。账户里的签名校验只包含了 nonce 和调用数据,没有包含链 ID。
于是用户在 A 链上签的一次转账,被原样拿到 B 链上重放了一次。
这和上一章跨链消息的唯一 ID 必须包含目标链是同一条规则:任何会被验证的签名,都必须绑定它适用的范围——链、合约地址、nonce,一个都不能少。
改一个变量
体验明显变好:用户一个月不用再授权一次。
风险同时放大三个维度:泄露后的暴露窗口长了 720 倍;这期间你的应用可能已经升级、新增了一些当初授权范围没考虑到的函数;用户早就忘了自己授权过什么。
如果一定要长有效期,必须用累计额度和更窄的函数白名单把它补回来,并且给用户一个能一眼看到「我授权了什么、还剩多久」的界面。
有效期和范围是一对跷跷板。 放长一边,另一边就要收紧。
合谋和社工的风险大幅下降——设备不会被说服。
但可用性风险大幅上升:三台设备可能放在同一个家里,一次火灾、一次搬家、一次盗窃就全没了。 社交恢复的原始假设是「这些守护人不会同时失效」,同地点的设备违反了这个假设。
更根本的是,它把恢复从「社交」变回了「保管」——你又回到了那个只有一把钥匙的保险柜,只是钥匙变成了三把、放在同一个抽屉里。
好处是真实的:修漏洞、加功能、适配新标准,都不需要用户迁移资产。
代价是它给账户加了一个全新的、位于所有规则之上的权限:能升级实现的那个人,可以一次性改掉全部验证逻辑。 会话密钥的范围、恢复的时间锁、代付的白名单,全部失效。
T8 那三条代理铁律在这里一条都不能少,而且要加一条属于账户场景的:升级权限应该归账户主人自己,而不是归应用开发者。 一旦是后者,用户的资产安全性就等于你的私钥安全性——你又变成了托管方,只是没有说出来。
带走的问题
用户是谁?这一章的答案决定了整个技术选型。如果用户是加密原生的人,EOA 加好引导可能完全够用;如果用户是第一次听说私钥的人,那抄助记词这一步就是一道天花板。不要为了用上新技术而用,要为了那个具体的人。
谁在支付?账户抽象最直观的改变,就是把「发起交易的人」和「支付 Gas 的人」拆开了。拆开之后这个问题必须被重新回答:代付方的钱从哪来、什么情况下不付、被薅光之后产品还能不能用。 答不上来的赞助方案,上线后都会变成一次运营事故。
Agent 有什么权限?会话密钥就是这个问题在合约层的答案:有效期、目标、函数、单笔、累计、能否再授权。这六项写进合约,就是一把可以安全交给程序的密钥。 T32 的策略引擎跑在链下,这里跑在链上,两者最好同时存在。
本章自测
抽象的是「谁能签名」这件事本身。
在 EOA 里,这是一个密码学事实:对应私钥签的名就是有效的,没有中间状态,也没有条件。在智能账户里,它变成了一段代码:这段代码可以检查时间、金额、目标、函数、调用次数,也可以要求多方共同确认。
一句话:从「密码学事实」变成了「可编程的策略」。
代价是你要为这段策略负责——它写错了,就是漏洞。
不够,而且差得很远。
只限制目标合约时,会话密钥仍然可以调用那个合约上的任何函数,包括授权类的函数。一次授权把额度给了攻击者控制的地址,之后的转账根本不经过这个账户,所有金额限制都不会被触发——因为授权操作本身的金额是零。
规则是:策略必须覆盖所有会改变资金控制权的动作,而不只是转账。 授权、委托、改管理员、升级,一个都不能漏。
因为打包者要在链下先模拟一遍验证,模拟通过才会打包上链。
如果验证依赖外部状态,就可能出现「链下模拟通过、链上执行失败」的情况。这时候交易已经发出去了,打包者白付一次 Gas。构造这种操作的成本极低,重复几百次就能让打包者拒绝服务你的账户。
所以验证阶段被严格限制,它的本质是一个反 DoS 设计,不是洁癖。
能挡住的:守护人合谋,前提是账户主人在窗口期内看到了提议并且执行了取消。
挡不住的至少三类:
- 用户在窗口期内失联——而最需要恢复的场景往往就伴随着失联。这是时间锁的结构性盲区。
- 守护人不独立——用户以为选了五个人,其实是一个人的五个地址。阈值形同虚设,链上看不出任何异常。
- 通知渠道本身被攻破——用户收不到提议通知,时间锁就是一段没人看的等待。
所以时间锁必须配两样东西:链上事件加可靠的链下通知,以及主密钥无条件、无延迟的取消权。三者缺一,守护人就从帮手变成了可以夺取账户的人。
没有标准答案,检查这几件事:
- 表里每一行都能在代码里找到出处吗?还是有些权限只存在于文档里?
- 每一把密钥的六个维度都填了吗:有效期、目标、函数、单笔、累计、能否再授权。
- 代付方在什么情况下拒绝付费?这条规则写在合约里还是写在后端?
- 恢复提议会发链上事件吗?通知怎么送到用户手里?
- 谁能升级账户合约?升级权限归用户还是归你?有没有时间锁?
- 所有会改变资金控制权的动作,都被策略覆盖了吗?不只是转账。
- 出事时,你能不能从链上事件还原出「哪一把密钥、在哪一步、做了什么」?
第 5 条是最容易被跳过、也是最能决定性质的一条:升级权限归你,你就是托管方,无论产品文案怎么写。
一句话带走
账户抽象把「谁能签名」变成一段可编程的策略。