在过去一周(2026/06/08 - 2026/06/15),以太坊和 Solana 上共检测到 4 起值得关注的安全事件,总损失约为 598 万美元。下表列出了具有代表性的事件:
| 日期 | 事件 | 类型 | 预计损失 |
|---|---|---|---|
| 2026/06/08 | Flooring Protocol | 整数溢出 | ~$900K |
| 2026/06/09 | Top Token | 治理攻击 | ~$1.59M |
| 2026/06/10 | Raydium(Solana 上) | 缺乏输入验证 | ~$1.34M |
| 2026/06/15 | Aztec | 缺乏输入验证 | ~$2.15M |
- Aztec:Rollup 证明路径与 L1 结算路径之间存在验证缺口,导致两者处理了不同的交易集合,最终达到不一致的状态。
- Raydium:缺少验证检查,使攻击者得以操纵 LP 代币赎回计算,从四个流动性池中耗尽全部储备。
Web3 最佳安全审计机构
在上线前验证设计、代码与业务逻辑
本周重点:Aztec
在本次事件中,由于单个参数未受约束,ZK 证明验证器与 L1 结算逻辑处理了不同的交易集合。这种证明与结算之间的一致性缺口,适用于任何将两条路径作为独立代码运行的 Rollup 设计。
2026 年 6 月 15 日,以太坊上专注于隐私的 Rollup 项目 Aztec Connect 遭到攻击,损失约 215 万美元 [1]。根本原因在于已验证的 Rollup 交易集合与 L1 结算处理边界之间存在不匹配,导致 ZK 证明路径与结算逻辑处理了不同的交易列表。攻击者利用这一缺口在 Rollup 状态中虚增未经支撑的存款余额,随后通过正常的结算流程将其提取。
背景
Aztec Connect 是以太坊上一个专注于隐私的 Rollup 项目,支持在 L2 上进行私密交易。由于用户资金源自 L1,必须先存入 Rollup 处理器合约,才能在 L2 默克尔树中以备注形式表示。
存款过程分为两个阶段:
第一阶段: 用户调用 depositPendingFunds(),通过 increasePendingDepositBalance() 增加 userPendingDeposits[assetId][owner] 的值,并将代币转入 RollupProcessor。这会在 L1 上创建一笔待处理存款。
function depositPendingFunds(uint256 _assetId, uint256 _amount, address _owner, bytes32 _proofHash) external {
increasePendingDepositBalance(_assetId, _owner, _amount);
// ... 将代币转入合约
}
第二阶段: 用户提交存款证明,该证明随后被纳入 Rollup 并添加到 L2 状态。当 processRollup() 执行时,decodeProof() 从编码的 calldata 中读取 numTxs,并将其与解码后的证明数据一起返回。两者随后被传入 processRollupProof():
function processRollup(bytes calldata, bytes calldata _signatures) external {
(bytes memory proofData, uint256 numTxs, uint256 publicInputsHash) = decodeProof();
processRollupProof(proofData, _signatures, numTxs, publicInputsHash, rollupBeneficiary);
}
在 processRollupProof() 内部,两个函数被顺序调用。首先,verifyProofAndUpdateState() 针对所有解码后的交易验证 ZK 证明并更新 Rollup 状态。然后,processDepositsAndWithdrawals() 处理 L1 结算,仅遍历前 _numTxs 个槽位,并对每笔存款调用 decreasePendingDepositBalance()(若用户在第一阶段未实际存款,此调用将回滚,从而将 Rollup 信用与真实的 L1 转账绑定):
function processRollupProof(bytes memory _proofData, bytes memory _signatures,
uint256 _numTxs, uint256 _publicInputsHash, address _rollupBeneficiary) internal {
verifyProofAndUpdateState(_proofData, _publicInputsHash); // 证明路径:所有已解码交易
processDepositsAndWithdrawals(_proofData, _numTxs, _signatures); // 结算路径:仅前 _numTxs 个
}
// processDepositsAndWithdrawals 内部:
end := add(proofDataPtr, mul(_numTxs, TX_PUBLIC_INPUT_LENGTH))
while (proofDataPtr < end) {
// ... 对于每笔存款:
decreasePendingDepositBalance(assetId, publicOwner, publicValue);
}
这种两阶段设计要求 L1 结算逻辑处理的交易集合与 ZK 证明验证的交易集合完全一致。若两条路径在处理哪些交易上存在不匹配,存款就可能在 Rollup 状态中被记入,却未消耗 L1 上对应的待处理余额。
漏洞分析
在 Rollup 处理器合约(0x7d65...2728)中,numTxs 并未被有效地约束为 ZK 证明所强制执行的交易集合。因此,证明路径与结算路径可能处理不同的交易列表。
在链下的 rollup_circuit 中,num_txs 作为见证值被加载,并仅受范围约束。电路使用它来控制哪些槽位被视为真实交易,但并不验证 num_txs 是否等于非填充证明的实际数量:
const auto num_txs = uint32_ct(witness_ct(&composer, rollup.num_txs));
field_ct(num_txs).create_range_constraint(MAX_TXS_BIT_LENGTH);
// ...
auto is_real = num_txs > uint32_ct(&composer, i); // 按槽位控制真实交易逻辑
证明者可以将 num_txs 设置为允许范围内的任意值。超出 num_txs 的槽位仍会被递归验证,但其公开输入被置零,因此不会对 Rollup 状态产生贡献:

在 Solidity 侧,decodeProof() 从 calldata 元数据中读取 numTxs,而该元数据并未被复制到由 verifyProofAndUpdateState() 验证的重建 proofData 中。因此,结算循环的边界同样不受 ZK 证明的覆盖:

由于两侧都未对该值加以约束,攻击者可以将 numTxs 设置为低于实际解码交易数量的值。结算循环随后会跳过那些证明已在 Rollup 状态中记入信用的交易。一笔不可执行的交易可占据第一个已解码槽位(位于结算扫描范围内),而一笔真实存款则可位于后续槽位(由电路证明,但位于结算扫描范围之外)。证明会在 Rollup 状态中记入该存款,但结算逻辑会完全跳过它,包括对 decreasePendingDepositBalance() 的调用。这使得 L1 上的待处理存款余额未被消耗,而 Rollup 状态已将其反映为存款。
攻击分析
以下分析基于交易 0x074ec9...9aeeb1。
攻击者分两个阶段利用了证明路径与结算路径之间的缺口。
第一阶段:构建未经支撑的余额
-
步骤 1:攻击者提交了多个 Rollup 批次,每个批次包含两笔已解码交易:槽位 1 为一笔不可执行(垃圾)交易,槽位 2 为一笔真实存款,同时将
numTxs设置为 1。L1 结算逻辑仅处理槽位 1 的垃圾交易,完全跳过了槽位 2 的真实存款。 -
步骤 2:然而,ZK 证明验证并记入了所有已解码交易,包括槽位 2 的存款。由于结算逻辑从未处理该存款,
decreasePendingDepositBalance()未被调用,L1 上的待处理存款余额保持未消耗状态。攻击者对七种不同资产重复了这一模式,在 Rollup 状态中积累了未经支撑的余额。
第二阶段:提取资金
- 步骤 3:一旦七种未经支撑的余额建立完毕,攻击者便针对每种资产发起标准提款。由于这些余额在 Rollup 状态中确实存在,结算逻辑认为这些提款合法,L1 合约因此释放了相应资金——总计约 215 万美元。

结论
该漏洞并非密码学层面的弱点,而是 Rollup 架构中两条关键代码路径之间的状态一致性缺陷。根本原因在于:numTxs 在两侧均未被绑定到已证明的交易集合。电路仅对其进行了范围约束,而 Solidity 解码器则从未经验证的 calldata 元数据中读取该值。缺乏这一绑定,证明路径与结算路径便可处理不同的交易列表。攻击者将 numTxs 设置为低于实际交易数量的值,使结算逻辑跳过了证明已在 Rollup 状态中记入信用的存款。由此产生的未经支撑的余额随后通过正常的结算流程被提取。
Aztec Connect Rollup 宣布了退役计划,交易处理与提款原计划于 2024 年 3 月 31 日终止 [2]。然而,Rollup 处理器合约仍于 2024 年 4 月 10 日通过一个拉取请求进行了升级 [3],而漏洞逻辑正存在于该退役后的升级版本中。
修复方案需要将 numTxs 绑定到 ZK 证明所验证的完整交易集合,确保两条路径始终处理相同的交易集合。任何将证明验证与 L1 结算分离的 Rollup 设计,都必须强制要求两条路径在一个相同的、可验证边界的交易集合上运行。哪怕仅有一个参数存在差异,都可能将一个本来健全的证明系统转变为构建未经支撑余额的攻击向量。
参考资料
本周更多事件
Raydium
2026 年 6 月 10 日,Solana 上 Raydium 旧版 AMM v3 程序的四个流动性池遭到攻击,损失约 134 万美元 [1]。提款处理程序未验证调用者提供的账户是否与池中存储的对应账户匹配,因此攻击者替换了一个受控账户以操纵赔付计算。同一手法在数秒内耗尽了四个流动性池的全部储备。
背景
Raydium 的 AMM 是 Solana 上的一个恒定乘积做市商。每个池持有两个代币金库,并铸造代表储备比例份额的 LP 代币。当流动性提供者提款时,处理程序按比例计算赔付,并转移两个金库的相应份额:
coin_out = total_coin * withdraw_amount / lp_supply
pc_out = total_pc * withdraw_amount / lp_supply
在 Solana 上,每种代币类型由一个 Mint 账户定义,其中存储总供应量、精度和铸造权限。每位持有者的余额存储在与该 Mint 绑定的独立 Token 账户中——一个 Mint 可对应不同持有者的多个 Token 账户。这与 EVM 不同,EVM 中单个 ERC-20 合约在内部同时管理代币定义和所有余额。
在上述提款公式中,lp_supply 从池的 LP Mint 账户中读取——该账户追踪 LP 总供应量。计算的正确性依赖于该值来自真实的 LP Mint。然而,在 Solana 上,调用者按位置将每个账户传入指令,因此处理程序必须验证每个调用者提供的账户与池状态中存储的规范账户相匹配。
漏洞分析
被攻击的程序(27haf8...8vQv)未开源,且其可执行数据(ProgramData)在攻击后已被关闭,无法直接进行字节码检查。以下分析基于从程序最后一次升级缓冲区重建的字节码,并与链上交易行为相互印证。
在提款处理程序中,调用者传入的 LP Mint 账户未被绑定到池中记录的 amm.lp_mint。以下从链上字节码重建的伪代码展示了账户布局。处理程序检查了池状态、PDA 权限、两个金库及用户账户的绑定关系——但未检查槽位 5 的 LP Mint:
let amm_info = next_account_info(it)?; // accounts[1] — 池状态(持有 amm.lp_mint)
// ...
let amm_lp_mint_info = next_account_info(it)?; // accounts[5] — 调用者提供的 mint
let amm = AmmInfo::load(amm_info)?;
// 此处检查了权限、金库、open_orders 的绑定关系...
// >>> 缺失:检查 accounts[5].key == amm.lp_mint <<<
let lp_mint = Mint::unpack(&amm_lp_mint_info.data.borrow())?;
let lp_mint_supply = lp_mint.supply; // 从未经验证的 mint 中读取
let coin_amount = total_coin * withdraw_amount / lp_mint_supply;
let pc_amount = total_pc * withdraw_amount / lp_mint_supply;
由于 LP Mint 账户未被绑定,攻击者可以替换为一个完全由其控制的 Mint 账户。将其总 supply 设置为 1 并销毁 1 个代币,赔付比例即为 1 / 1 = 100% 的储备。
该漏洞代码自 2023 年 1 月 3 日程序最后一次升级以来一直存在且未作修改,距漏洞被利用已过去约 1,254 天。
攻击分析
以下分析基于交易 1csN6v...3s7s。
- 步骤 1:攻击者创建了一个伪造的 LP Mint 账户,设置
decimals = 0,总supply = 0。

- 步骤 2:攻击者初始化了一个与伪造 LP Mint 绑定的 Token 账户,然后以 Mint 权限向其铸造恰好 1 个代币,将 Mint 的总
supply固定为 1。

- 步骤 3:攻击者调用提款函数,在预期账户槽位中传入伪造的 LP Mint,并将步骤 2 中的 Token 账户(持有 1 个伪造 LP 代币)作为 LP 来源。由于
withdraw_amount = 1,lp_supply = 1,处理程序计算出total_coin * 1 / 1和total_pc * 1 / 1,相当于两种储备的 100%(对于 RAY/USDC池,即 893,700USDC和 66,837RAY)。

- 步骤 4:处理程序销毁了攻击者的 1 个代币,并将两个池金库的全部储备转出,从而完全耗尽了 RAY/
USDC池。

攻击者在约 15 秒内对另外三个池重复了相同的操作。四个池的被盗金额合计如下:
| 池 | 被盗金额(约) |
|---|---|
| RAY/USDC | ~66,837 RAY + ~893,700 USDC |
| RAY/wSOL | ~74,720 RAY + ~5,603 wSOL |
| RAY/SRM | ~8,622 RAY + ~10,692 SRM |
| RAY/Sollet ETH | ~5,038 RAY + ~16 Sollet ETH |
结论
根本原因是单个缺失的账户验证检查:提款处理程序使用调用者提供的 Mint 账户的 supply 作为 LP 供应量除数,却未将其绑定到池中记录的 amm.lp_mint。在 Solana 上,每个调用者提供的账户都必须绑定到池状态中存储的对应规范账户。正确的实现应拒绝任何密钥与池存储记录不匹配的 LP Mint,并从池内部的 LP 计数器而非外部提供的 Mint supply 计算赎回量。被攻击的合约是一个较旧的部署版本(最后一次升级于 2023 年 1 月),并于攻击当天被关闭。据 Raydium 团队表示,全额赔偿将由 Raydium 国库承担 [1]。
参考资料
- [1] Raydium 事后声明
关于 BlockSec
BlockSec 是一家全栈区块链安全与加密合规服务提供商。我们构建产品与服务,帮助客户在协议和平台的完整生命周期内进行代码审计(涵盖智能合约、区块链与钱包)、实时拦截攻击、分析安全事件、追踪非法资金,并满足反洗钱/反恐融资合规要求。
BlockSec 已在顶级学术会议上发表多篇区块链安全论文,披露了多个 DeFi 应用的零日攻击,成功拦截多起黑客攻击事件并挽救了逾 2000 万美元的资产,同时保障了数十亿美元加密资产的安全。
-
官方 Twitter 账号:https://twitter.com/BlockSecTeam
-
🔗 BlockSec 审计服务 : 提交申请



