Aztec Connect 被盗 219 万美元资产分析

本次攻击的核心教训是:ZK-Rollup 合约的结算循环上界必须与 ZK 公开输入哈希的承诺范围严格对齐。

文章作者、来源:慢雾科技

背景

2026 年 6 月 14 日,已弃用的 Aztec Connect RollupProcessor 合约(0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455)遭到利用。攻击者通过构造 numRealTxs 与 decoded_slots 之间的边界缺口,在单笔原子交易中从 L1 资金池提取了约 219 万美元的资产。Aztec Connect 已于 2024 年 3 月弃用,但该不可变合约因持有遗留用户资产而持续暴露风险。本文从合约源码与链上 calldata 两个维度还原本次攻击的完整技术细节。

攻击概览

漏洞根因

RollupProcessorV3 在处理 rollup 结算时,L1 结算循环的遍历范围与ZK 公开输入哈希的承诺范围之间存在结构性缺口。攻击者利用这一缺口,使 31/32 个公开输入槽的内容被 ZK 证明承诺进 L2 状态根,却不经过 L1 合约层的任何结算校验。

Decoder.sol:numRealTxs 完全由攻击者控制

numRealTxs 从 calldata 偏移 4516 处读取,且没有任何 on-chain 约束:

decoded_slots 向上取整至 numTxsPerRollup 的倍数,这是 SHA256 预编译的数据布局要求。但这一取整操作在 numRealTxs 与 decoded_slots 之间制造了一个攻击者可以自由填充的 gap 区域。

RollupProcessorV3.sol:结算循环仅覆盖 numRealTxs 个槽

 

安全假设的断裂

正常流程的安全假设是:每个公开输入槽要么经过 L1 合约层校验(存款时扣减 pendingDepositBalance),要么被 ZK 电路约束 publicValue == 0。但在本漏洞场景中:

  1. SHA256 预编译覆盖全部 32 个槽(实测输入 8192 字节 = 32 × 256B),gap 槽的内容已被 ZK 证明承诺。
  2. L1 结算循环仅遍历前 1 个槽,gap 槽 [2..32] 不受任何 L1 层面校验。
  3. ZK 电路对 gap 槽 publicValue 的约束(应为 0)被攻击者绕过或未施加。

三条防线互相依赖,但没有一条能够独立为 gap 槽提供安全保障——当 ZK 电路约束缺失时,L1 合约层同样无法察觉。

双路径分歧模型

同一份 calldata 被两条路径以不同上界消费:

两条路径对"哪些槽算数"的认知不一致(ZK 认 32 个、L1 只认 1 个),正是凭空铸造的根源。

攻击流程

攻击交易 0x074ec931…aee1 共包含 14 次 processRollup() 调用,呈"前 7 铸造 + 后 7 提款"的两段式结构,全部在单笔原子交易内完成。

第一阶段:铸造 — 在 L2 凭空获得资产(Rollup #13277–13283,7 次)

1.攻击者 EOA 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 调用总控入口合约 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd,触发选择器 0x6f3ce701。

2.总控合约依次调度 3 个中继合约,每个中继合约硬编码了若干份恶意 rollup calldata。每份 calldata 的关键参数:

  • numRealTxs = 1,rollupSize = 1024,numInnerRollups = 32
  • 第 1 个槽(L1 可见):proofId = 0(noop),publicValue = 0
  • 第 2–32 个槽(31 个 gap 槽,L1 不可见):proofId = 1(存款),publicValue = N,publicOwner = 攻击者 L2 地址
  • 附带对应 ZK 证明(电路未约束 gap 槽 publicValue 为 0)

3.中继合约 A 依次调用 RollupProcessor.processRollup()(Rollup #13277–13281,5 次):

  • Verifier 验证 ZK 证明通过 — SHA256 承诺覆盖全部 32 槽
  • L1 结算循环 end = 1 × TX_PUBLIC_INPUT_LENGTH = 1 槽,仅处理 noop
  • gap 槽 [2..32] 的伪造存款被 ZK 承诺进新 Merkle root → 攻击者 L2 余额获得 5 × 31N

4.中继合约 B 以同样方式处理 Rollup #13282–13283(2 次),攻击者 L2 余额再获 2 × 31N。至此,攻击者 L2 账户累计获得 7 × 31N 的无支撑充值,而 L1 资金池未减少一分钱。

第二阶段:提款 — 将 L2 虚增余额兑现为 L1 资产(Rollup #13284–13290,7 次)

攻击者以 7 次提款 rollup 将铸造阶段获得的全部 L2 余额兑换为 L1 资产:

  1. Rollup #13284(DAI):withdraw() → RollupProcessor 直接 transfer 270,513.054 DAI → 0x0f18…edd17
  2. Rollup #13285(wstETH):transfer 167.890 wstETH → 攻击者
  3. Rollup #13286(yvDAI):transfer 4,873.857 yvDAI → 攻击者
  4. Rollup #13287(yvWETH,中继合约 C 接管):transfer 16.570 yvWETH → 攻击者
  5. Rollup #13288(LUSD):transfer 9,273.734 LUSD → 攻击者
  6. Rollup #13289(yvLUSD):transfer 359.047 yvLUSD → 攻击者
  7. Rollup #13290(ETH,最后一笔):RollupProcessor 通过内部 CALL 转出 908.987 ETH → 攻击者。

单笔原子交易执行成功(gasUsed = 4,513,539),合约层面不可部分回滚。攻击者净获约 $2.19M,全部来自 RollupProcessor 的合法用户资产池。

资金追踪

根据链上取证追踪(截至 2026-06-15,案发约 1 天后),被盗资金的状态如下:

所有资产在单笔交易内从 RollupProcessor 经中间攻击合约 0x06f585…d0fcD 直接转入攻击者 EOA 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17。中间合约未持有残余资金。

当前被盗资金 100% 原封不动停留在攻击者 EOA,尚未开始洗钱。

总结

本次攻击的核心教训是:ZK-Rollup 合约的结算循环上界必须与 ZK 公开输入哈希的承诺范围严格对齐。在 L1 合约层的循环边界 numRealTxs 与 SHA256 承诺的 decoded_slots 之间存在差距时,任何依赖 ZK 电路对 gap 槽施加约束的安全假设都可能被攻击者绕过——L1 必须对每一个被 ZK 证明承诺的公开输入槽进行独立校验,而不能将校验责任外包给电路层。

慢雾安全团队建议项目方在 Rollup 系统部署前进行完整的外部安全审计,重点关注 L1/L2 状态分界线上的逻辑一致性、calldata 解码的可信边界、以及 ZK 公开输入的 on-chain 二次验证。对于已弃用但持有遗留资产的合约,建议执行有序资产迁移或销毁以消除持续暴露的风险。

本文由 SlowMist 威胁情报团队结合 MistEye 威胁情报系统、MistTrack 追踪平台及 SlowMist Agent AI 驱动分析编写,有任何问题欢迎咨询反馈。