KKDEMIAN

EIP-7702 的签名流程:从一个哈希到被改写的账户代码

7702 的签名不是一笔交易,而是一份可被任何人提交的代码控制授权。签名者只对 chain_id、委托目标和 nonce 三个字段负责,签名对象是 keccak256(0x05 ‖ rlp([chain_id, address, nonce])),既没有 EIP-712 的结构化语义,也没有过期时间。nonce 是唯一的失效机制 —— 而预签 50 条不同 nonce 的授权,正是为了让「取消」这件事失去意义。

思考

本文是机制说明,不是事件报道。规范依据为 EIP-7702 定稿版本。

TL;DR

7702 的签名本质上是一份可被任何人提交的代码控制授权,不是一笔交易。

  • 签名者只对三个字段负责:chain_id、委托目标 address、账户 nonce。签名对象是 keccak256(0x05 ‖ rlp([chain_id, address, nonce]))。
  • 它不是 EIP-712。 没有 domainSeparator、没有 primaryType、没有字段名 —— 除非固件专门实现 0x05 的解析,用户看到的就是一串 32 字节哈希。
  • 没有过期时间。 nonce 是唯一能让一张已签名的授权失效的东西。
  • 所以 nonce spray 的真正目的不是提高命中率,而是让「取消」失去意义 —— 烧掉一个 nonce 只废掉一张,其余仍然有效。
  • 即使外层交易执行失败,委托也不会回滚。 一笔看起来「失败」的 Type-4 交易,代码可能已经装好了。
  • 任何持有 ETH 的账户都能替签名者提交并付 gas。签名与上链是分离的两件事。

一、签名对象:三个字段、一个魔数

一条授权元组由六个值组成,但真正被签名的只有前三个:

字段 长度 含义
chain_id 32 字节 授权生效的链;0 表示所有链通用
address 20 字节 委托目标合约;0x0 表示清除委托
nonce 8 字节 必须等于签名账户当时的 nonce
y_parity 1 字节 secp256k1 恢复标识(0/1)
r 32 字节 ECDSA 签名
s 32 字节 ECDSA 签名,且必须 ≤ secp256k1n/2(EIP-2 低 s 要求)

签名哈希的定义非常直接:

msg = keccak256(0x05 ‖ rlp([chain_id, address, nonce]))

0x05 是 7702 专用的魔数,用来和普通交易签名(0x00/0x01/0x02/0x03)区分开。

它不是 EIP-712 的 domainSeparator 结构,也没有 primaryType、字段名和钱包可渲染的语义。 结果是:硬件钱包和签名界面拿到的是一串 32 字节哈希,除非固件专门实现 0x05 的解析,否则用户实际是在盲签一个自己看不懂的授权。

这是 7702 与传统交易签名最本质的区别。 普通交易签名至少绑定了 to、value、data、gas;7702 授权只绑定「把账户代码指向谁」。

它不承诺目标合约会做什么,也不承诺代码之后不会变 —— 如果目标地址是可升级代理,签名时是良性实现,升级后就可以变成 sweep 合约。

两个边界值得记住:authority 必须能通过 ecrecover 恢复,因此合约账户不能作为授权者;授权签名本身不能广播,必须由一条 Type-4 交易携带,但携带者可以是任何人。

二、上链处理:两层签名与九个步骤

一条 Type-4 交易里其实有两层签名:

  1. 发送者签名 —— 签整条交易的 RLP 载荷(含 authorization_list),决定谁付 gas、谁的 nonce 被消耗;
  2. 授权者签名 —— 对每个元组单独签名,决定哪个账户的代码被改写。

两者可以完全无关。 这正是「赞助」(sponsorship)的设计初衷:dApp 替用户付 gas,用户只签名授权。也正是币安案中 operator 能替 Binance 14 提交授权的原因。

链上处理顺序是:先递增发送者 nonce,再处理授权列表,最后执行 calldata。对每个元组依次执行九步:

1. chain_id == 0 或 == 当前链 ID
2. nonce < 2^64 - 1
3. authority = ecrecover(msg, y_parity, r, s),且 s <= secp256k1n/2
4. 把 authority 加入 accessed_addresses(预热)
5. authority 的代码为空,或已经是 0xef0100 开头的委托
6. authority 的当前 nonce == 元组 nonce
7. 若 authority 非空,向全局退款计数器加 12,500 gas
8. 写入 code = 0xef0100 ‖ address;若 address 为 0,则清空代码
9. authority.nonce += 1

任何一步失败,只跳过该元组,不回滚整笔交易,继续处理下一条。同一 authority 出现多条有效元组时,以最后一条有效的地址为准。

Gas 结构解释了那笔 1,258,562

规范定义两个常量:PER_EMPTY_ACCOUNT_COST = 25,000,PER_AUTH_BASE_COST = 12,500。

预收按条数,退款按有效性:交易内在成本里按 25,000 × 授权列表长度预收,只对 authority 非空的元组向全局退款计数器加 12,500。

于是币安那笔授权交易的账单可以精确复原:

50 × 25,000  = 1,250,000   50 条元组预收
− 12,500                   唯一有效元组命中已存在账户
+ 21,000                   交易内在 gas
+ 62                       calldata 等
= 1,258,562

一分不差。 这条账还顺带说明了一件事:49 条诱饵不是免费的,每条实打实烧掉 25,000 gas,而发送者为真假元组一并付费。

三、nonce:唯一的「有效期」

7702 没有过期时间。nonce 是唯一能让一张已签名的授权失效的东西。 理解这一点,才能理解整个攻防博弈。

第一,授权 nonce 与普通交易 nonce 是同一个计数器。 签署并上链一条授权,会把账户 nonce 推高 1;这会让一笔同 nonce 的待处理普通交易永久失效。反过来,如果账户先发了一笔普通交易,之前签好的同 nonce 授权也会失效。

第二,多条元组按顺序校验,nonce 会边处理边涨。 以币安案的窗口(18,914,775–18,914,824,实际 nonce 809)为例:

排列 结果 nonce 变化
升序 775→824 775–808 过低跳过,809 命中后 810…824 依次生效 连推 16 次(命中那条 + 其后 15 条)
降序 824→775 824–810 过高跳过,809 命中,808–775 过低跳过 只涨 1 次

攻击者选择降序,正是为了只让一条生效 —— 避免把热钱包自己的交易队列打乱。

第三,nonce 也是防御手段,但会被「喷洒」破解。 如果怀疑某条恶意授权在 mempool 里,账户可以立刻发一笔 0 值自转交易,用掉当前 nonce,让那条授权失效。

但攻击者预签 50 条不同 nonce 的授权后,烧掉一个 nonce 只会废掉其中一张,其余 49 张仍然有效。

这才是 nonce spray 的真正目的:不是提高命中率,而是让「取消」这件事失去意义。

第四,chain_id = 0 是跨链重放开关。 设 0 时,同一条签名在所有 EVM 链上都有效,只要各链 nonce 恰好匹配。币安案的 50 条全部是 chain_id = 1,所以那条授权无法在 BNB Chain、Polygon 等链上重放 —— 多链流出必须另有解释。

四、执行上下文:签名之后账户变成了什么

委托生效后,账户的 code 字段变成 23 字节的 0xef0100 ‖ address。此后:

  • 任何对它的 CALL、DELEGATECALL、STATICCALL,都会加载目标合约的代码,但在 EOA 自己的上下文里执行:address(this) 是 EOA,msg.sender 是调用者,存储读写落在 EOA 的存储上;
  • 对下游代币合约来说,msg.sender 就是该钱包本身,因此不需要任何 ERC-20 approve;
  • 代码读取出现不对称:EXTCODESIZE 返回 23(委托指示符的长度),而委托执行中的 CODESIZE 返回目标合约代码的长度。规范明确「EXTCODEHASH 等外部读取操作不自动跟随委托,而作用在委托指示符本身」—— 这意味着常规的「EOA 检测」(EXTCODESIZE == 0)会失效,但委托目标是可以直接从链上解出来的;
  • 委托链只跟随第一跳:如果目标合约本身也是委托账户,客户端不会继续递归;
  • 委托不会因为外层交易 revert 而回滚。 规范原文是「已处理的委托指示符不会被回滚」。一笔看起来「失败」的 Type-4 交易,仍然可能已经把代码装好了 —— 这对模拟和监控是个陷阱。

清除委托只有一条路:用当前 nonce 再签一条 address = 0x0 的授权,把代码重置为空。没有管理员、没有时间锁、没有链上开关。

在清除之前,所有新入账资产都自动暴露在委托逻辑之下。

五、套回币安案:签名流程在哪一步失效

把上面的机制套回 10-29 的事件,流程可以逐步复原:

  1. 攻击者构造 50 条元组,chain_id = 1、address = 0x63e842…、nonce 从 18,914,824 递减到 18,914,775;
  2. 签名侧对这 50 个哈希逐一签名 —— 这需要 50 次 ECDSA,正常人工操作几乎不可能,通常意味着自动化签名服务或批量预签;
  3. operator 把 50 条签名打包进一条 Type-4 交易,自己作为发送者支付 0.006545 ETH(约 14.5 美元);
  4. 上链时账户 nonce 是 18,914,809,只有这一条命中;代码写入,nonce 变为 18,914,810;
  5. 3 分 24 秒后,operator 发一条普通交易调用 sweep(address,address[]),代码在热钱包上下文里执行,转走 167,464 ETH、16.15 亿 USDT 和 64 个代币;
  6. 此后 3.5 小时内又调用 12 次,其中多次是 23,100 gas 的空轮询。

失效点不在链上,而在第 2 步:签名侧把一个「改写账户代码」的请求,当成了普通消息签名来处理。

如果签名策略能解析 0x05 载荷,它会看到三件事 —— 目标是一个未验证合约、nonce 窗口跨 50 个值、chain_id 为主网。这三条里任何一条都足以拦截。但它看到的可能只是一段十六进制哈希。

六、结论

EIP-7702 的签名流程可以压缩成一句话:用私钥对「链 ID + 目标地址 + nonce」签一个哈希,任何人把它包进 Type-4 交易,账户代码就被永久改写。

它没有过期、没有撤销按钮、没有执行回滚,唯一的失效机制是 nonce 被消耗 —— 而这一条已经被 nonce spray 针对性地绕过了。

因此,安全的签名流程必须把重心放在签名之前,而不是链上监控:

  1. 解析 0x05 载荷并展示目标地址与 chain_id
  2. 对委托目标做代码哈希白名单,拒绝未验证或可升级合约
  3. 把「代码委托」与「资产转移」拆成两套密钥和两套审批
  4. 对热钱包直接禁用 Type-4
  5. 签名后监控 mempool 中是否有人替你提交

底线判断。 7702 把「签名」从一次具体操作的授权,升级成了「账户控制权的托管」。

判断一个钱包或交易所是否真正理解这一点,只看一件事:它的签名界面能不能在用户按下确认前,把 0xef0100‖address 里的那个地址,用人话显示出来。显示不出来,流程再多重也只是盲签。

参考资料


配套阅读:EIP-7702 攻击链复盘:18 小时准备,一条签名,一个机器人、币安热钱包失守。本文为机制说明,规范依据 EIP-7702 定稿版本。DYOR。

相关主题:⛓️ L1 / L2
订阅更新

在 Telegram 或 X 关注每周研究、市场信号与上币动态。

更多文章: 思考查看全部
币安还没公开那份签名日志 —— 截至 10-30 凌晨的披露核对

六条公开表态,零条技术细节。币安至今没有官方确认攻击向量是 EIP-7702,没有发布签名请求记录、HSM/MPC 审计轨迹、策略引擎放行日志或第三方取证报告,也没有面向用户的正式事件公告。更值得注意的是口径:独立核算的外流 56.43 亿减去已冻结 26.2 亿约为 30 亿,而 CZ 认可的损失量级是 10 亿左右 —— 差三倍,说明「损失」从未被定义。

2026年10月30日
币安给出根因:被攻破的不是 EIP-7702,是一个有签名能力的服务身份

10-30 02:23 UTC 的《Security Incident Update》第一次给出根因口径:一名 DevOps 工程师的笔记本被攻陷,攻击者由此进入服务容器并提交未授权提现;币安称未发现私钥泄露证据,已撤销受影响地址的授权。总影响估算 63 亿、已冻结 30 亿、净敞口约 33 亿。这把事件从「协议漏洞」拉回「运维终端 → 服务凭证 → 签名能力」,7702 不是根因,而是让一笔受限额约束的提现变成一次不受限额约束的代码执行。

2026年10月30日
币安热钱包失守:一条合法签名,8 分钟,56 亿美元

攻击者在 15:59 部署好恶意委托合约,20:24 用一条由热钱包私钥签署的 EIP-7702 授权完成清空,20:32 才被分析师察觉,21:01 起发行方开始冻结。真正被抢走的时间窗口只有几分钟,而能挽回损失的手段 —— 稳定币黑名单 —— 只覆盖约 45%,原生 BTC/ETH 仍在攻击者手中。这不是合约被黑,合约没有漏洞;被攻破的是「谁有权让钱包执行代码」。

2026年10月30日