事件背景:一个已弃用合约的致命一击
2026年6月14日,以太坊主网上一个早已被官方弃用的合约突然被攻击者精准利用,一次性从资金池中抽走了约219万美元的资产。受害合约正是Aztec Connect的RollupProcessorV3代理(地址0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455)。Aztec Connect作为曾经知名的ZK-Rollup隐私交易协议,早在2024年3月就已停止运营并弃用了大部分核心设施,但遗留的用户资产因合约的不可变性而长期滞留池中。这一次,攻击者没有攻击任何活跃的网络,而是直指这份沉睡的“遗产”,用一种极为巧妙的方式撕裂了L1与L2的状态一致性。
更令人震惊的是,整个攻击过程仅通过一笔原子交易完成,攻击者甚至用了一套精心编排的智能合约组合,把铸造虚增余额和提款套现一次性打包,让传统风控根本没有拦截的余地。本文将结合链上calldata与合约源码,全面还原此次攻击的技术细节,并深入探讨ZK-Rollup当中容易被忽视的结算边界问题。
攻击概览:一笔交易,七轮铸造,七轮提款
攻击交易哈希为0x074ec9317d8336db37e8c348fbdd7515573ff4088239c77ab429f522509aeeb1。攻击者EOA为0x0f18d8b44a740272f0be4d08338d2b165b7edd17,他部署了一个总控入口合约0x06f585f74e0da633ae813a0f23fb9900b61d0fcd,并配合三个中继合约来组装恶意calldata。整个攻击过程在单笔交易中执行了14次processRollup()调用,形成“前7次铸造+后7次提款”的两段式结构,由于都在同一原子交易内,即使后续部分失败,整个交易也会回滚,但攻击者完美地让所有操作一次性成功。
获利资产明细让人触目惊心:908.987 ETH、270,513.054 DAI、167.890 wstETH、16.570 yvWETH、4,873.857 yvDAI、9,273.734 LUSD和359.047 yvLUSD,全部属于RollupProcessor合约中未迁移的用户资金。值得注意的是,这些资产最终都停留在攻击者EOA中,截至案发一天后,尚未发生任何混币或转移行为,似乎攻击者在等待更安全的时机进行清洗。
漏洞根因:numRealTxs与被忽略的空槽
要理解这次攻击的根源,必须先从RollupProcessorV3如何处理calldata说起。该合约在接收rollup证明数据时,会解析出一个关键参数numRealTxs,它代表本次rollup中实际包含的用户交易数量。在合约源码中,numRealTxs由calldata偏移4516字节处直接读取,并且没有任何范围检查,完全由调用者任意指定。
Decoder.sol中的致命取整
在Decoder.sol汇编代码中,有这样一段逻辑:
// 从calldata读取numTxs
numTxs := and(calldataload(add(inPtr, NUM_REAL_TRANSACTIONS_OFFSET)), 0xffffffff)
let numTxsPerRollup := div(rollupSize, numInnerRollups)
let numFilledBlocks := div(numTxs, numTxsPerRollup)
numFilledBlocks := add(numFilledBlocks,
iszero(eq(mul(numFilledBlocks, numTxsPerRollup), numTxs)))
decodedTransactionDataSize := mul(mul(numFilledBlocks, numTxsPerRollup), TX_PUBLIC_INPUT_LENGTH)
这里的关键是numTxsPerRollup。在Aztec Connect的设计中,rollupSize为1024,numInnerRollups为32,因此每个内部rollup(即每批公开输入槽)需要容纳32笔交易的公开输入。当攻击者设置numTxs=1时,numFilledBlocks经过div(1,32)=0后的向上取整操作变成1,于是decodedTransactionDataSize=1*32*TX_PUBLIC_INPUT_LENGTH=32个槽位。也就是说,虽然只有1笔真实交易,但解码器会天真地认为需要32个公开输入槽。这就产生了一个长达31个槽的“间隙”,而这些间隙中的内容可以由攻击者在calldata中自由填充。
L1结算循环仅覆盖numTxs个槽
再看RollupProcessorV3的processDepositsAndWithdrawals()函数中的循环边界:
end := add(proofDataPtr, mul(_numTxs, TX_PUBLIC_INPUT_LENGTH));
while (proofDataPtr < end) {
...
if (proofId == 1) {
decreasePendingDepositBalance(assetId, publicOwner, publicValue);
}
if (proofId == 2) {
withdraw(publicValue, publicOwner, assetId);
}
unchecked { proofDataPtr += TX_PUBLIC_INPUT_LENGTH; }
}
这里end的计算直接使用了攻击者控制的_numTxs=1,因此循环只会处理1个公开输入槽。对于解码器生成的额外31个槽,L1合约视而不见,完全跳过,不会执行任何存款扣减或提款校验。
三条防线同时失效
正常流程下,安全依赖三个层次:
- SHA256预编译承诺:它对所有32个槽进行哈希,gap槽的内容也被承诺进L2的状态根,这部分正常执行了,攻击者无法绕过。
- ZK电路约束:理论上,电路应该强制gap槽的
publicValue为零,以防止凭空铸造。然而本次攻击中,攻击者提交的证明成功通过验证,说明电路对gap槽缺乏有效约束,或者攻击者找到了绕过的方法(例如使用了一个旧版证明,该版本存在漏洞)。 - L1合约层校验:此层对gap槽完全未检查,因为它只循环
numTxs次。
这三条防线互相依赖,当ZK电路这一环出现疏漏时,L1合约并没有独立的安全兜底,于是攻击者得以在L2侧凭空铸造大额资产。
双路径分歧:ZK认32个,L1只认1个
如果我们把整个数据流抽象出来,就会看到一种典型的“双路径分歧”模型。同一份calldata被两条路线解析:
- ZK证明路径:SHA256预编译消费全部32个公开输入槽,形成状态根哈希。
- L1结算路径:仅处理
numTxs=1个槽,gap槽内所有存款(proofId=1)和提款(proofId=2)均被忽略。
攻击者正是利用了两条路径对“哪些槽是有效数据”的认知差异。他在gap槽中填入proofId=1(存款)和极高publicValue,并将publicOwner设为攻击者在L2的地址。因为这些槽被ZK承诺,L2认为攻击者收到了相应的资金;而L1却不执行decreasePendingDepositBalance,资金池毫发无损。等到攻击者在L2余额被虚增后,他再发起正常的提款rollup,将这部分凭空生成的余额兑换成L1的ETH和各种代币。整个过程就像在银行系统里,左边柜台承认了你存入的支票,右边柜台却不知道有这笔存款,但你转头去提款时,柜员发现账户余额确实增加了,于是乖乖付钱。
攻击流程的拆解:从铸造到提款一气呵成
分析该笔交易的内部调用轨迹,可以清晰地分为两个阶段。攻击者部署的总控合约0x06f585f74e0da633ae813a0f23fb9900b61d0fcd通过选择器0x6f3ce701被触发,然后依次调度三个中继合约,每个中继合约封装着预置的恶意rollup calldata。
第一阶段:铸造——L2凭空获得资产
在Rollup #13277至#13283的7次调用中,每次processRollup都包含了精心构造的证明数据:
numRealTxs=1- 第1个槽
proofId=0(noop),publicValue=0 - 第2至32槽,整整31个槽全部设为
proofId=1(存款),publicValue为一个巨额的代币数量,publicOwner指向攻击者控制的L2地址。
因为L1只循环第一个槽,看到noop就跳过了,不做任何扣款。但SHA256却将这31个槽的存款信息真实地哈希进新的状态根,ZK电路也没有阻拦。于是,攻击者在L2侧的账面余额凭空暴涨,相当于用空气向系统存入了巨额资金。经过7次类似的铸币操作,攻击者L2账户累计获得了天文数字的代币,而这些代币在L1资金池中根本不存在对应的储备。
第二阶段:提款——将虚增余额兑现为L1资产
铸造完成后,攻击者毫不拖泥带水,立即在随后的7次rollup(#13284至#13290)中发起提款。这些提款rollup完全是合法的:攻击者用L2上已经存在的虚增余额作为输入,生成标准提款证明。L1合约在processDepositsAndWithdrawals中检测到proofId=2,就从RollupProcessor的资金池中直接转账给攻击者EOA。具体提款顺序及资产如下:
- #13284:270,513.054 DAI
- #13285:167.890 wstETH
- #13286:4,873.857 yvDAI
- #13287:16.570 yvWETH
- #13288:9,273.734 LUSD
- #13289:359.047 yvLUSD
- #13290:908.987 ETH
所有资产在单笔交易中直接转入攻击者EOA,中间合约仅起到短暂中介的作用。这笔交易消耗了约451万gas,可见其计算复杂度之高,但gas费用对于219万美元的收益而言微不足道。
资金追踪与当前状态
根据慢雾安全团队的链上取证,截至2026年6月15日,被盗资金依旧沉淀在攻击者地址0x0F18D8b44a740272f0be4d08338d2b165b7EdD17中,尚未出现任何洗钱或分散转移的迹象。这在以往的黑客事件中并不常见,可能是攻击者在观察链下执法机构的反应,或者等待更深的流动性渠道出现。无论如何,这些资产目前处于完全裸露的监控之下,给追踪和潜在追回留下了一线机会。
值得一提的是,Aztec Connect团队早已停止该合约的维护,也无法通过合约升级来阻止后续攻击。此次事件再次暴露出已弃用合约携带遗留资产的风险。即使协议已经宣布终止,只要合约代码存在且持有资金,它就是一个不设防的金库。
技术教训与防护建议
从根本上看,这次漏洞并不是因为某个复杂的密码学缺陷,而是源于L1合约层与ZK证明层之间的边界对齐失误。在整个管道中,多个模块各自为政地进行数据解析,缺乏统一的“可信长度”校验。攻击者仅仅通过操控numRealTxs这个简单参数,就在两条解析路径之间撕开了一个致命的缺口。
对于任何采用ZK-Rollup架构的项目,以下几点防御措施至关重要:
- 公开输入槽的数量必须被on-chain严格验证:L1合约不能信任calldata中的
numRealTxs,而应该基于自身计算的decoded_slots来设定循环边界,或者至少强制要求numRealTxs等于数据块总数。 - 防御性重算验证:合约可以在执行结算前,自行用同样的SHA256预编译对全部槽位重新计算一次哈希,并与证明中的值做比对,确保没有任何槽位被跳过。
- 电路层独立安全:ZK电路必须对每个公开输入的合法性进行硬约束,无论L1如何处理。特别是gap槽的
publicValue应当被强制为零,不能完全依赖L1合约的循环来过滤。 - 遗留资产管理:对于已经弃用的合约,项目方应当及时发起资产迁移或通过多签执行自毁并将资金转至安全地址,避免成为攻击目标。不可变性的另一面是,漏洞一旦存在就永远存在。
总结
Aztec Connect被盗事件是一堂价值219万美元的公开课,它再次证明在跨层系统(尤其是L1与L2之间)中,每一处数据解析的边界都必须被严格对齐。攻击者并不需要破解zk-SNARK的数学难题,只需要在合法模块之间的“灰色地带”插入一个恶意参数,就能让系统自洽地制造出上百万美元的虚假资产。这暴露了当前许多ZK-Rollup项目在设计时过度依赖电路层假设,而忽视了L1合约层作为最后堡垒的责任。慢雾安全团队建议所有项目方在部署前进行多层次的外部审计,重点审查L1/L2状态分界线上的逻辑一致性,并在整个生命周期内保持对遗留风险的监控与管控。
在区块链的世界里,代码即法律,但法律一旦有了漏洞,惩罚便会来得无比猛烈。


评论 18
CryptoHawk
cry***k@proton.me2026年07月20日 08:42numRealTxs这个参数居然没有范围检查,这种低级错误审计都没发现吗?🤔
0xResearcher
0xr***r@protonmail.com2026年07月19日 22:05分析得很清晰,但攻击者用的旧版证明具体是哪个版本?公开了吗?求指路🔍
DevMolly
mo***v@gmail.com2026年07月19日 21:35我以前也遇到过calldata解析不一致的情况,当时是off-by-one,这个更隐蔽!
安全小白
736***0@qq.com2026年07月19日 10:30看完背后发凉,我们DApp也用了ZK,这种隐藏槽的问题之前从没考虑过。现在联系你们审计来得及吗?
Web3Codex官方
Official@web3codex.io2026年07月19日 11:26别担心,及时发现就不晚。请通过官网Telegram、邮箱或右侧客服按钮立即联系我们,我们的专家会第一时间响应。
合约老兵
old***t@126.com2026年07月19日 00:19官方已弃用,黑客还盯着不放,所以智能合约一旦部署就不能完全废弃啊,长教训了💡
zkBuilder
zk_***r@163.com2026年07月18日 23:17这个ZK电路约束缺失是致命伤,贵团队能提供电路审计吗?我们项目也准备上zk,想咨询。
Dev_Sam
de***m@outlook.com2026年07月18日 17:20numFilledBlocks的取整逻辑简直是埋雷,我们项目也用了类似代码,我得赶紧回去检查😱
晓风
xiao***o@gmail.com2026年07月18日 14:55攻击者的合约组合设计很有水平,你们能还原整个调用链吗?想学习一下防御思路。
Web3Codex官方
Official@web3codex.io2026年07月18日 16:11我们已完整还原攻击流程,会在后续文章中详细拆解。关注Web3Codex不迷路。如需技术交流,欢迎联络。
Alice_BTC
al***c@gmail.com2026年07月17日 14:38L1和L2状态分歧的问题是不是所有rollup都面临?有没有通用解决方案?
数字游民王
527***5@qq.com2026年07月16日 20:57已弃用的合约还能被攻击,链上遗留资产真是定时炸弹啊。
小明区块链
178***0@qq.com2026年07月16日 19:22一笔原子交易完成14次rollup调用,攻击者的工程能力太强了,好奇他是不是团队内部人员?
侦探迷
det***n@gmail.com2026年07月16日 15:53攻击者的EOA还没有转移资产,是在等闪电贷清洗吗?或者他根本就是白帽?🤔
Web3Codex官方
Official@web3codex.io2026年07月16日 16:37目前难以判断,但黑产清洗通常更快。我们持续监控相关地址,有新动向会及时更新。
RollupBuilder
104***3@qq.com2026年07月16日 08:52这种漏洞感觉是可以预防的,你们有没有针对rollup的安全加固方案?想合作。
DeFi先锋
def***r@outlook.com2026年07月15日 21:27看完想挖你们来做安全顾问,我们团队需要全方位的链上威胁建模,怎么联系?📩
安全老炮儿
le***c@outlook.com2026年07月15日 17:00分析得太透彻了,能把弃用合约的漏洞揪出来,Web3Codex技术深度真的强👍