Crypto OS
Technical Crypto OS第五阶段 · Infrastructure & Security

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 里这两件事是同一件事:签名对了就执行,没有中间状态。智能账户把它们拆开,于是「认不认」变成了一段你可以自己写的代码

一笔操作的完整流转

用户不再直接发交易,而是发一份操作意图,由别人打包上链。

  1. 用户签一份操作意图
  2. 进入专用的待处理池
  3. 打包者挑出来组成一笔交易
  4. 入口合约逐个分发
  5. 账户合约验证
  6. 代付方决定付不付
  7. 账户合约执行
用户从「发交易的人」变成了「签意图的人」,Gas 的支付者和交易的发起者被分开了

这条链上有三个角色是 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 个,恢复机制本身失效无关,这是可用性失效
主人反向滥用账户主人用恢复机制夺回一个已经承诺给他人的账户无关,这是治理问题

第四行值得反复强调。时间锁的全部前提是用户在窗口内能看到通知并且有能力取消。这个前提在最需要恢复机制的那些场景里恰恰不成立——一个人之所以丢了密钥,往往就是因为发生了让他没法正常上网的事。

所以设计恢复流程时,有三条比「选几个守护人」重要得多:

  1. 恢复提议必须产生一个链上事件,并且要有链下通知的通道。 用户看不见的提议,等于没有时间锁。
  2. 当前主密钥的取消权必须是无条件、无延迟的。 取消这个动作本身不能再有任何门槛。
  3. 守护人的独立性要被验证,而不是被假设。 至少记录每个守护人是怎么被确认的、什么时候确认的。

还有一条属于产品而不属于代码:时间锁的长度是一次权衡,不是一个技术参数。 短了防不住合谋,长了让真正需要恢复的人等太久。不同金额的账户应该给不同的长度,这和 T17 那张按业务风险分档的确认深度表是同一个思路。

它叫什么

智能账户Smart Account

账户本身是一份合约,「谁能签名」由合约里的一段代码回答。

它和 EOA 的根本区别不是功能多少,而是:EOA 的权限是密码学事实,智能账户的权限是一段可被阅读、测试和修改的规则。

操作意图UserOperation

用户签署的不是一笔交易,而是一份「我想做什么」的结构化对象:目标、数据、金额、Gas 上限、代付方。

它由别人打包上链,所以发起交易的人和支付 Gas 的人可以是不同的人。这一条是零 Gas 体验的全部来源。

打包者Bundler

把多份操作意图组装成一笔真正的交易并发上链的角色。

它属于传输层:没有它操作会卡住,但它伪造不了任何东西。 它会先在链下模拟验证阶段,模拟不过就不打包——这就是验证阶段被严格限制的原因。

入口合约EntryPoint

所有智能账户共用的分发与计费入口。账户合约只信任来自它的调用。

它是这套体系里最大的共同依赖:一旦它出问题,所有账户一起出问题,所以它的代码必须是整条链上被审计得最彻底的合约之一。

代付方Paymaster

替用户支付 Gas 的合约。可以是项目方赞助,也可以是用户用其他代币折算支付。

它的核心设计不是「怎么付」,而是「什么情况下不付」。没有拒绝规则的代付方,就是一个对所有人开放的取款机。

会话密钥Session Key

一把被限定了范围的临时密钥:有效期、目标合约、函数选择器、单笔上限、累计上限。

范围定义就是它的全部。 只限制目标合约不限制函数,是这一类实现最常见的漏洞——一次授权类调用就能绕过所有金额限制。

社交恢复Social Recovery

主密钥丢失时,由若干个预先指定的守护人共同发起替换的机制。

它必须同时具备三件事才成立:时间锁链上事件加链下通知当前主密钥的无条件取消权。缺任何一件,守护人就从「恢复的帮手」变成了「可以夺取账户的人」。

动手

动手部署一个智能账户,配置会话密钥与代付,完成一次用户零 Gas 的操作任意 Solidity 框架 + 本地节点或测试网0 元。全程本地网络或测试网,Gas 用水龙头领的测试币;不要把这套代码部署到主网,更不要往里放真实资产

全程本地网络或测试网。 这个 Lab 会故意留出漏洞再补上,任何一步都不该碰真钱。

验收标准四条,每一条都是一个会红会绿的测试:

验收 1:主密钥能执行任意操作
验收 2:会话密钥只能执行范围内的操作,范围外必须 revert
验收 3:用户账户里一分钱原生代币都没有,操作仍然成功
验收 4:会话密钥无法通过授权类调用绕过额度限制

先写一个最小账户,只有主密钥。

把本章那段 SmartAccount 精简到只剩 ownernoncevalidateexecute,部署到本地网络。

第一个测试就写负面用例:用一个随机地址直接调 execute,必须 revert。

先写这个测试,再写功能。 账户合约的第一条命是访问控制,不是功能。

接入入口合约,走一次完整流程。

用你的框架里对应的做法构造一份操作意图,签名,交给打包者,观察它落链。

重点看三件事:

- 链上那笔交易的 from 是谁?(不是用户)
- Gas 是从哪个账户扣的?
- 你的 validate 被调用了几次?

第一个问题的答案会让很多人第一次真正理解这套体系:用户的地址从此不再出现在交易的发起方字段里。 你的索引器、你的风控、你的「按发起方过滤」的查询,全都要跟着改。

加会话密钥,先只限制目标合约。

授予一把会话密钥,只允许它调用你的应用合约。写测试验证:调用别的合约会 revert。

然后做这个 Lab 最重要的一次攻击练习:用这把会话密钥,对你的应用合约调用一个授权类的函数,把一笔额度授给另一个你控制的地址,再用那个地址把钱取走。

它会成功。范围里没有函数选择器这一项,额度限制就是纸糊的。

亲手做成一次,比读十遍「要限制函数选择器」有用得多。

补上函数选择器白名单和累计额度,把上一步的攻击测试跑成红色。

这是这个 Lab 的核心验收:同一个测试,在补漏之前必须通过(攻击成功),补漏之后必须失败(攻击被拦)。

编译通过不算修好,测试从绿变红才算。

验证累计额度的扣减时机。

写一个测试:在一笔交易里让同一把会话密钥连续执行多次小额操作,总额超过累计上限。

如果 spent 是在执行之后才更新的,这个测试会通过,也就是攻击成功。把它改成验证阶段就扣,测试变红。

记住这个形状:状态更新晚于使用。下一章会用同一个形状把一个合约的钱全部取走。

加代付方,实现零 Gas。

写一个最简单的代付方,先不加任何限制,让用户在账户余额为零的情况下完成一次操作。

然后立刻加限制:单账户次数上限、全局日预算、目标合约白名单、一个紧急开关。

加完之后写一个「薅羊毛」测试:用一个账户连续发起大量高 Gas 的无意义操作,验证它在达到上限后被拒绝。

加社交恢复,然后自己攻击它。

实现守护人、阈值、时间锁、取消。跑通正常恢复流程之后,写三个测试:

- 够数的守护人发起恢复,主密钥在窗口内取消:恢复必须失效
- 够数的守护人发起恢复,窗口内无人取消:恢复必须生效
- 恢复提议必须发出一个链上事件,事件里带发起人、新主密钥、生效时间

第三条不是功能,是用户能不能知道自己正在被夺取账户的唯一依据。没有它,时间锁只是一段没人看的等待。

最后做一次账本。 把这个账户的全部权限列成一张表:

能做什么上限有效期谁能撤销
主密钥任意操作永久只有恢复流程
会话密钥白名单里的函数单笔与累计到期自动失效主密钥,立即
守护人集合发起恢复提议只能换主密钥常驻主密钥,窗口期内
代付方替这个账户出 Gas日预算常驻紧急开关

每一行都要能从代码里找到出处。填不满的格子,就是你还没想清楚的权限。

AI Lab

AI Lab让 AI 设计一套社交恢复流程,你负责写出它会被滥用的三种场景Level 2 · AI Copilot

分三步,第三步是真正的考题。

第一步:
为一个智能账户设计社交恢复流程。
给出完整的状态机:有哪些状态、什么事件触发状态转移、
每个状态下谁能做什么。用合约函数的粒度描述,不要用抽象说法。

第二步:
列出这套流程的全部信任假设,每一条写成
「如果 ___ 作恶或失效,账户会被 ___」。
主语要具体到角色和数量。

第三步:
现在扮演攻击者。你的目标是夺取一个使用了这套流程的账户。
给出至少 6 条攻击路径,每一条包含:
- 你需要什么前提条件
- 具体的操作步骤
- 这套流程的哪一个环节没有挡住你
- 账户主人有没有机会发现
不要只考虑技术手段,社会工程和时机选择也算。

第一步模型通常给得很完整,因为社交恢复是一道有标准答案的题。要盯的只有两处:有没有时间锁,以及主密钥的取消权有没有被加上奇怪的前置条件。

第二步会暴露它是不是真的理解了。很多回答会写「如果守护人作恶,账户会被夺取」——这句话是对的,但没有信息量。追问下去:几个守护人?在什么时间窗口内?账户主人此时在做什么?把这三个变量填进去,才是一条可用的信任假设。

第三步是这个 Lab 的价值所在,而且模型在这一问上表现相当好——枚举攻击路径本来就是它擅长的任务。常见的产出包括:挑用户出差时发起、伪造一个「安全升级」的通知让用户自己批准、先攻破通知渠道再发起恢复、长期潜伏等到某个守护人换设备时补位。

但有两条它很少主动提出,需要你自己补上:

第一,守护人不独立。 用户以为自己选了五个朋友,实际上其中三个是同一个人的三个地址。阈值在这种情况下完全失效,而链上看不出任何异常。

第二,恢复机制的反向滥用。 账户主人自己用恢复流程,去夺回一个他已经承诺交给别人的账户。这不是安全漏洞,是治理漏洞——而它在多人共管的账户里是真实存在的风险。

最后那条验证必须你自己做:把三种滥用场景写成会跑的测试。 一个描述出来的攻击和一个能跑绿的攻击测试,可信度差一个数量级。这一点从 T8 起就是同一条标准。

AI 说完之后,你必须自己验证

  • 它的方案里有没有时间锁:没有时间锁的社交恢复,等于把账户直接交给守护人集合
  • 当前主密钥能不能无条件、无延迟地取消一次恢复提议:有任何前置条件都要追问为什么
  • 恢复提议有没有产生链上事件,以及链下通知怎么送达:用户看不见的提议等于没有时间锁
  • 它有没有考虑守护人不独立的情况(同一个人的多个地址),以及怎么缓解
  • 它有没有考虑用户在窗口期内失联:这是时间锁的结构性盲区,回避它的方案是不完整的
  • 它有没有考虑守护人自己丢失密钥导致凑不够阈值:恢复机制自己也会失效
  • 它给的时间锁长度是不是一个拍脑袋的常数,有没有按账户金额分档
  • 最后自己写出三种滥用场景,每一种都要能在测试里复现,不能只是描述

真实案例

智能账户部署后没人初始化,被别人抢先初始化反复出现的模式

很多智能账户是「先部署一个最小代理,再由用户调用一次初始化设定主密钥」。如果初始化函数没有限定调用者,部署和初始化之间的那个窗口里,任何人都可以把自己设成主人

批量部署账户的场景下,这个窗口可能长达几分钟,而且是公开可见的。

这和 T8 里那个 2017 年的库合约案例是同一个形状:初始化路径没有保护。 正确做法是把初始化和部署放进同一笔交易,并且在初始化函数里加上「只能被调一次」的检查。

代付方被高 Gas 的无意义操作薅干常见运营事故

一个项目方为了拉新,赞助所有用户的 Gas,没有设任何上限。

有人写了个脚本,不断发起消耗极高但毫无意义的操作,几个小时内把赞助池花光。所有正常用户在那之后都用不了。

这不是攻击者偷走了资产,而是他让项目方替他烧掉了资产。 防御手段很朴素:单账户次数与金额上限、全局日预算、只赞助白名单里的目标合约。

一条更普遍的教训:任何「替别人付费」的设计,都必须先写清什么情况下拒绝付费。

会话密钥只限制了合约,没限制函数这一类实现最常见的漏洞

一把会话密钥被限定为「只能调用游戏合约」,单笔上限设得很小。

攻击者拿到这把密钥后,没有去转账——他调用了游戏合约上一个授权类的函数,把一个大额度授给了自己控制的地址,然后用那个地址把钱取走了。

整个过程中,金额上限一次都没有被触发,因为授权操作本身的 value 是零

这是 T32 那个案例在账户层的重演:限额只管转账,攻击者就去改控制权。

签名没有绑定链 ID,在另一条链上被重放多链部署时的典型事故

一份智能账户合约被部署到多条链上,同一个用户在每条链上的地址相同。账户里的签名校验只包含了 nonce 和调用数据,没有包含链 ID

于是用户在 A 链上签的一次转账,被原样拿到 B 链上重放了一次。

这和上一章跨链消息的唯一 ID 必须包含目标链是同一条规则:任何会被验证的签名,都必须绑定它适用的范围——链、合约地址、nonce,一个都不能少。

改一个变量

如果会话密钥的有效期从 1 小时改成 30 天

体验明显变好:用户一个月不用再授权一次。

风险同时放大三个维度:泄露后的暴露窗口长了 720 倍;这期间你的应用可能已经升级、新增了一些当初授权范围没考虑到的函数;用户早就忘了自己授权过什么。

如果一定要长有效期,必须用累计额度和更窄的函数白名单把它补回来,并且给用户一个能一眼看到「我授权了什么、还剩多久」的界面。

有效期和范围是一对跷跷板。 放长一边,另一边就要收紧。

如果守护人从 5 个朋友换成 3 台你自己的硬件设备

合谋和社工的风险大幅下降——设备不会被说服。

但可用性风险大幅上升:三台设备可能放在同一个家里,一次火灾、一次搬家、一次盗窃就全没了。 社交恢复的原始假设是「这些守护人不会同时失效」,同地点的设备违反了这个假设。

更根本的是,它把恢复从「社交」变回了「保管」——你又回到了那个只有一把钥匙的保险柜,只是钥匙变成了三把、放在同一个抽屉里。

如果用户改成用某种代币而不是原生代币支付 Gas

体验上是一次改进:用户不再需要为了发交易而先去买原生代币。

但它引入了两个新依赖。一个是价格:代付方需要知道这种代币值多少 Gas,而价格来源一旦可被操纵,代付方就会以极低的价格接收付款——T21 讲过的那条路在这里原样适用。另一个是代币本身:非标准代币的转账可能带钩子、可能收税、可能在某些情况下静默失败,T9 列过的每一种怪癖,在这里都会变成代付方的风险。

结论:接受哪些代币付 Gas,是一份白名单,不是一个开关。

如果账户合约本身是可升级的

好处是真实的:修漏洞、加功能、适配新标准,都不需要用户迁移资产。

代价是它给账户加了一个全新的、位于所有规则之上的权限:能升级实现的那个人,可以一次性改掉全部验证逻辑。 会话密钥的范围、恢复的时间锁、代付的白名单,全部失效。

T8 那三条代理铁律在这里一条都不能少,而且要加一条属于账户场景的:升级权限应该归账户主人自己,而不是归应用开发者。 一旦是后者,用户的资产安全性就等于你的私钥安全性——你又变成了托管方,只是没有说出来。

带走的问题

4
用户是谁?

用户是谁?这一章的答案决定了整个技术选型。如果用户是加密原生的人,EOA 加好引导可能完全够用;如果用户是第一次听说私钥的人,那抄助记词这一步就是一道天花板。不要为了用上新技术而用,要为了那个具体的人。

5
谁在支付?

谁在支付?账户抽象最直观的改变,就是把「发起交易的人」和「支付 Gas 的人」拆开了。拆开之后这个问题必须被重新回答:代付方的钱从哪来、什么情况下不付、被薅光之后产品还能不能用。 答不上来的赞助方案,上线后都会变成一次运营事故。

16
Agent 有什么权限?

Agent 有什么权限?会话密钥就是这个问题在合约层的答案:有效期、目标、函数、单笔、累计、能否再授权。这六项写进合约,就是一把可以安全交给程序的密钥。 T32 的策略引擎跑在链下,这里跑在链上,两者最好同时存在。

本章自测

一句话带走

账户抽象把「谁能签名」变成一段可编程的策略。

做完这一章的动手环节了?勾上它查看全部进度

本页目录