在过去一周(2026/07/20 - 2026/07/26),以下 8 起重大安全事件值得关注,涉及总损失约 $39.5M。
| 日期 | 事件 | 类型 | 预计损失 |
|---|---|---|---|
| 2026/07/19* | Allbridge | 输入验证缺陷 | ~$1.65M |
| 2026/07/20 | Zilliqa | 随机数生成缺陷 | ~$400K |
| 2026/07/20 | Wanchain | 消息编码缺陷 | ~$500K |
| 2026/07/22 | 42DAO | 私钥泄露 | ~$900K |
| 2026/07/22 | AFX Trade | 私钥泄露 | ~$24.15M |
| 2026/07/22 | B² Network | 私钥泄露 | ~$3.8M |
| 2026/07/23 | Verus | 私钥泄露 | ~$7.6M |
| 2026/07/24 | Lien Finance | 验证逻辑缺陷 | ~$542K |
*Allbridge 事件发生于 7 月 19 日(UTC 17:51),未包含在上周的报告中,此处补充收录以保持完整性。
- Zilliqa 入选原因:该事件揭露了链下钱包实现中一个长期未被发现的漏洞,自 2019 年起便使使用旧版原生 ZIL 账户的用户面临私钥泄露风险。
- Allbridge 入选原因:该事件展示了由 Solana 位置化账户模型引发的账户别名漏洞。由于源代码不可用,该漏洞需从已部署的程序二进制文件中重建分析。
- Wanchain 入选原因:该事件展示了一座跨链桥如何在不破坏密码学的情况下被耗尽。签名有效且验证正确,然而非单射、无分隔符的消息编码使得对 3,097.56 个代币的授权在 Cardano 上被重新解释为 203,001,692.164714 个代币。
- Lien Finance 入选原因:该事件说明了仅验证余额数量匹配而不检查匹配项身份与重复性的校验机制是如何被绕过的。
Web3 最佳安全审计机构
在上线前验证设计、代码与业务逻辑
本周精选:Allbridge Core
本事件入选原因:账户别名模式适用于任何在两个指令角色中接受同一可变账户的 Solana 程序。其底层机制——对同一状态存在两个可变视图,第二次写入静默覆盖第一次——与经典的 ERC-20 自转账漏洞(transferFrom 中 from == to)结构相同。项目方的事后分析 [1] 提供了高层次摘要,但未详述代码层面的机制。以下分析从已部署的程序二进制文件中重建,以填补这一空白。
2026 年 7 月 19 日(UTC 17:51),Solana 上的跨链桥协议 Allbridge Core 遭到攻击,损失约 $1.65M(约 $1.12M 的 USDC 和约 $539K 的 USDT)。根本原因在于:swap 指令在接受同一 Pool 账户同时充当发送和接收角色时,未强制要求两者不同。当同一账户被传递两次时,一个角色的内部记账更新被另一个角色静默覆盖,而实际的代币转账已经完成结算。五次自兑换足以扭曲 Pool 的记录状态,使攻击者得以将少量 USDT 输入转换为约 $2.24M 的 USDC。
背景
Allbridge Core 是一个跨链桥,通过名为 vUSD 的内部记账单位路由兑换。一次兑换由两个记账步骤组成:
源代币 -- swap_to_v_usd(send_pool) --> vUSD
vUSD -- swap_from_v_usd(receive_pool) --> 目标代币
vUSD 并非 SPL 代币,它仅作为每个 Pool 链上数据中的一个字段存在。每个 Pool 跟踪 token_balance、v_usd_balance 和 reserves。发送步骤将源代币从用户转移至发送桥接金库,增加发送 Pool 的代币侧记账,并计算 vUSD 输出。接收步骤将该 vUSD 添加至接收 Pool,减少其代币侧记账,并将目标代币从接收桥接金库转移给用户。
Solana 程序以调用方传入的位置数组接收账户。程序的指令处理器指定哪个数组位置对应哪个角色(send_pool、receive_pool、send_mint 等),但运行时不阻止调用方在两个位置传入相同的账户地址。当程序在两个不同位置反序列化账户时,每次反序列化各自产生一个独立的内存对象,对一个对象的修改不会影响另一个对象。若同一账户被传递两次,两个对象从相同的链上数据出发,但一旦任一对象被修改,二者便开始分叉。
处理器结束后,程序的退出逻辑(如 Anchor 的 AccountsExit)按固定顺序将每个账户对象序列化回账户数据。若两个对象映射至同一账户,第二次序列化将完全覆盖第一次。
漏洞分析
受影响的 Allbridge Core 部署的确切源代码不可用。本分析从存储在 ProgramData 中的 1,770,736 字节 ELF 重建(SHA-256:40f776...346bb6)。最后一次部署插槽(204,727,029)早于攻击插槽(433,941,722)。
存在漏洞的程序为 BrdgN2...ceWB。在攻击交易的 Swap 指令(指令 3 至 7)中,四对发送和接收账户完全相同:
| 角色 | 账户 |
|---|---|
send_mint / receive_mint |
Es9vMF...wNYB |
send_pool / receive_pool |
DW4a2E...wCX |
send_bridge_token / receive_bridge_token |
2xY9TD...vohV |
send_user_token / receive_user_token |
817UdW...CVct |
解析器包含 16 个已还原的 32 字节比较,将每个角色绑定至其预期的 mint、金库、所有者、PDA、权限或 Token 程序。然而,没有任何比较强制要求 send_pool.key() != receive_pool.key()。两个 Pool 角色在处理器运行前各自独立反序列化。
已部署的 ELF 显示两个 Pool 构造、发送和接收计算,以及按固定顺序排列的两个 Pool 退出:
L54249-L54252 function_9881(accounts[5]) -> send_pool 本地对象
L54294-L54297 function_9881(accounts[6]) -> receive_pool 本地对象
L74177-L74195 function_12623(send_pool) -> swap_to_v_usd
L74368-L74389 function_12940(receive_pool) -> swap_from_v_usd
L55932-L55936 function_8300(send_pool) -> 完整 Pool 序列化
L55940-L55944 function_8300(receive_pool) -> 完整 Pool 序列化
每次 function_9881 调用分配一个独立的 176 字节本地 Pool 对象,并将解码字段复制到其中。当账户键相同时,两个对象从相同数据出发,但对发送对象的修改不会影响接收对象。
重建的伪 Rust 代码(已部署行为,非还原源码)[2] [3]:
let mut send_pool = Account::<Pool>::try_from(&accounts[5])?;
let mut receive_pool = Account::<Pool>::try_from(&accounts[6])?;
require_keys_eq!(send_pool.mint, send_mint.key());
require_keys_eq!(receive_pool.mint, receive_mint.key());
// 金库、所有者、PDA、权限和 Token 程序绑定均已检查。
// 缺失:require_keys_neq!(send_pool.key(), receive_pool.key());
token::transfer(send_user_token, send_bridge_token, amount)?;
let v_usd = swap_to_v_usd(&mut send_pool, amount)?;
let receive_amount =
swap_from_v_usd(&mut receive_pool, v_usd, receive_amount_min)?;
token::transfer(receive_bridge_token, receive_user_token, receive_amount)?;
// 处理器返回后的 AccountsExit:
send_pool.exit(program_id)?; // 第一次写入
receive_pool.exit(program_id)?; // 第二次写入,覆盖第一次
function_8300 从位置 0 开始写入,序列化所有 131 个已定义的 Pool 字节。第一次退出写入 send_pool;第二次退出将 receive_pool 写入相同字节上。因此,最终账户状态为过时的接收侧值,而非两次本地更新的合并。
SPL 代币转账是独立的 CPI。它们在 Pool 退出之前更新由 SPL Token 程序拥有的金库账户,因此第二次 Pool 序列化无法撤销输入转账。每次别名兑换因此将已转移的代币留在金库中,同时丢弃对应的发送侧 Pool 更新,而接收侧更新得以保留,使记录的 Pool 状态朝着更高的 v_usd_balance 和更低的代币侧记账方向移动。
核心缺陷在于程序未检查 send_pool 和 receive_pool 是否为不同账户。所有其他验证(mint 绑定、金库所有权、PDA 派生)均通过,因为两个角色合法地属于同一 Pool。
攻击分析
以下分析基于交易 3LNLaG...Y39Q。
该漏洞利用在一笔交易中完成,包含一次 Kamino 闪电借款、七条 Allbridge Swap 指令和一次 Kamino 还款。
- 步骤 1:攻击者从 Kamino 借入约 112 万
USDC,通过 Allbridge 兑换为约 94.9 万USDT(指令 1-2)。此次首次兑换为后续自兑换提供了所需的USDT,并同时改变了USDC和USDTPool 的状态。

- 步骤 2:攻击者使用完全相同的 mint、Pool、金库和用户代币账户作为发送和接收方,提交了五次约 10 万
USDT的自兑换(指令 3-7)。由于接收 Pool 每次调用最后被序列化,持久化的记账记录了接收侧的减少但未记录发送侧的增加。随着扭曲累积,观察到的输出逐次递减:
| 自兑换次序 | 输入 | 记录的 vUSD |
价格(vUSD/USDT) | USDT 输出 |
|---|---|---|---|---|
| 1 | ~10万 | ~16.2万 | ~5.24 | ~4.78万 |
| 2 | ~10万 | ~25.6万 | ~16.6 | ~2.54万 |
| 3 | ~10万 | ~47.1万 | ~57.2 | ~1.25万 |
| 4 | ~10万 | ~91.8万 | ~190 | ~5,730 |
| 5 | ~10万 | ~182万 | ~563 | ~2,360 |
五次调用共向金库转入约 50 万 USDT,返还约 9.38 万 USDT。Pool 记录的 token_balance 随每次调用下降,而真实金库余额增长,扩大了下一步可利用的差距。

- 步骤 3:攻击者仅投入约 3,990
USDT(指令 8)。已被扭曲的USDTPool 产生了约 224 万vUSD,USDCPool 将其转换为约 224 万USDC。膨胀的vUSD输出成为可能,是因为经过五轮丢弃发送侧更新后,Pool 记录的代币余额远低于其实际金库余额。

- 步骤 4:攻击者偿还了约 112 万
USDC的 Kamino 闪电贷本金及约 11.2USDC的手续费(指令 9)。攻击者最终余额为约 112 万USDC和约 53.9 万USDT。
结论
本次事件的根本原因是缺少对 send_pool 和 receive_pool 引用不同账户的验证。单一可变 Pool 被反序列化为两个本地记账对象,Token 程序完成金库转账后,第二次完整序列化覆盖了第一次,从而将 Pool 的记录账目与其真实金库余额分离。账户别名、ELF 控制流、序列化顺序、交易日志和金库余额均指向同一机制 [1]。
对于 Solana 程序,任何在两个或多个可变角色中接受相同账户类型的指令,都应强制要求唯一性(require_keys_neq!),或在退出前合并修改。Anchor 框架的 #[account] 约束系统默认不强制跨角色唯一性,因此必须显式添加此检查。通用模式——对同一状态存在两个可变视图,第二次写入静默覆盖第一次——凡程序自行管理序列化顺序之处均可出现。
参考资料
- [1] Allbridge Core,技术事后分析:Solana Pool Swap 漏洞利用
- [2] Allbridge Core JS SDK,Solana Swap 账户接口(
bridge.json) - [3] Allbridge Core EVM 合约,Pool 记账参考(
Pool.sol)
本周更多事件
Zilliqa Ledger 钱包
2026 年 7 月 20 日,Zilliqa 监测到链上活动与对旧版原生 ZIL 账户的主动漏洞利用一致,已知损失约 $400K。根本原因是 Zilliqa Ledger 应用 EC-Schnorr 签名路径中的随机数存在偏差:随机数生成代码在模数规约后从一个 40 字节缓冲区复制了错误的 32 字节,将最高有效 64 位固定为零。攻击者通过同一账户的若干公开签名,即可利用基于格的技术恢复私钥并清空账户。
背景
Zilliqa 原生交易使用 secp256k1 上的 EC-Schnorr 签名。每次签名时,签名者必须采样一个新鲜的、全宽度的、不可预测的临时随机数 ,满足 (secp256k1 曲线阶)。签名流程生成承诺 ,挑战 ,以及响应 ,其中 为私钥。广播后, 即为公开信息。
线性关系 是关键所在。正确的实现保持安全,因为每个 都是新鲜且均匀分布的。若随机数存在偏差或过小,每个公开签名都会泄露关于同一私钥的信息。
漏洞分析
相关随机数生成路径在 Zilliqa Ledger 应用的提交"根据 Ledger 团队审查修复漏洞"中引入。预期流程为:生成 40 字节随机数,对曲线阶 取模,将结果标量用作 。生成宽随机整数并对曲线阶取模本身并无问题,缺陷出现在向随机数缓冲区复制时:
unsigned char nonce[size+8];
cx_rng(nonce, size+8);
cx_math_modm(nonce, size+8, (unsigned WIDE char *) PIC(domain->n), size);
os_memcpy(T->K, nonce, size);
cx_math_modm 执行后,32 字节标量结果右对齐存储于 40 字节缓冲区中。os_memcpy(T->K, nonce, size) 复制前 size(32)个字节,保留了前导 8 个零填充字节,并丢弃了最后 8 个字节的熵。因此生成的随机数满足 而非 。
这意味着每个随机数的 64 位被固定为零。由 Schnorr 响应方程 可知,每个签名提供一个公开的模线性关系,其结果以 为界。这是一个隐藏数问题(HNP)实例。对同一密钥拥有约四个或更多受影响签名时,标准格规约算法可在普通硬件上于数秒内恢复私钥 [1]。
该缺陷自 2019 年起存在于 Zilliqa Ledger 应用的每个发布版本中,直至 2026 年 7 月事件被发现 [2]。
本事件不提供攻击分析。漏洞利用完全在链下执行:攻击者通过格规约从链上公开签名中恢复私钥,然后签署标准转账交易,不存在需要分析的多步链上攻击序列。
结论
本事件由链下签名实现缺陷引起,而非链上智能合约漏洞。Zilliqa Ledger 应用生成的 EC-Schnorr 签名随机数被约束为 ,在同一账户完成若干原生交易后,泄露了足够的结构化信息以恢复私钥。
主要缓解措施是密钥退役,而非仅更新 Ledger 应用。修复后的应用可防止新的弱化签名产生,但无法抹除已记录在链上的公开签名。任何已产生足够多脆弱签名的受影响密钥都必须视为已泄露 [2]。
对于钱包和协议团队:将随机数生成和编码视为安全关键代码,在适用时优先选用经过充分审查的标准确定性随机数生成方案,并为受影响用户提供协调的迁移路径,同时考虑攻击者可能已持有相同私钥而发起抢先交易的风险。
参考资料
Wanchain Cardano 跨链桥
2026 年 7 月 20 日,Wanchain Cardano 跨链桥因 Cardano TreasuryCheck Plutus 验证器中存在非单射消息编码,损失约 515.2M NIGHT(约 $500K)[1]。项目方的事后分析 [2] 描述了高层次机制,但未提供字节级编码细节或代码;以下分析重建了完整的碰撞过程。跨链桥节点签名有效且验证正确,但对可变长度字段进行的无分隔符拼接使攻击者得以移动两个相邻数字字段之间的字节边界,将对 3,097.56 NIGHT 的授权重新解释为提取 203,001,692.164714 NIGHT。
背景
受影响的组件是 Wanchain 的 Cardano 跨链国库系统。源链用户销毁或锁定代币,跨链桥节点对从解密器字段构造的授权消息进行签名,签名证明提交至 Cardano TreasuryCheck Plutus 验证器以从国库释放资产。
漏洞分析
TreasuryCheck 验证器对 14 个已解析解密器字段的原始拼接进行桥接节点签名验证:
hashRedeemer = sha3_256 $ mconcat
[ toPkhPay, toPkhStk, policy, assetName
, packInteger amount, packInteger adaAmount
, txHash, packInteger index, packInteger mode
, uniqueId, packInteger txType, packInteger ttl
, packInteger outputCount, userData
]
packInteger 使用的整数编码为可变长度,且拼接不含长度前缀、类型分隔符或域分隔的类型化序列化。因此,不同的语义元组可能产生相同的已签名字节串。
在代表性攻击中,跨链桥节点对一次正常提取进行了签名:
amount = 3097560000 -> packInteger = b8a103c0
adaAmount = 1206800 -> packInteger = 126a10
组合结果 = b8a103c0126a10
攻击者提交的 Cardano 解密器解析为:
amount = 203001692164714 -> packInteger = b8a103c0126a
adaAmount = 16 -> packInteger = 10
组合结果 = b8a103c0126a10
两个字节序列完全相同。签名检查通过,Cardano 合约将解密器解释为比授权金额大约 65,000 倍的提取操作。
攻击分析
以下分析参考 Cardano 代表性交易 0a4861...2ea1 和 BSC 源交易 0xe90111...d5f26b。
-
步骤 1:攻击者创建了外观正常的源链跨链桥请求。在代表性案例中,BSC 交易销毁了 3,110
NIGHT,Wanchain API 记录预期在 Cardano 接收 3,097.56NIGHT[3]。 -
步骤 2:跨链桥节点对从原始拼接字段构造的授权哈希进行签名。
-
步骤 3:攻击者在保持已签名字节序列不变的同时,改变了 Cardano 解密器中
amount和adaAmount之间的字段边界。 -
步骤 4:Cardano
TreasuryCheck验证器将解密器解析为高价值提取并成功验证了复用的签名。该交易向攻击者支付了 203,001,692.164714NIGHT。 -
步骤 5:相同模式在多笔 Cardano 交易中重复,最终共耗尽约 515,206,545.426856
NIGHT(约 $500K)。
结论
没有任何密码学被破解。签名者产生了有效签名,TreasuryCheck 验证器也正确地验证了它们。缺陷在于消息构造:hashRedeemer 将 14 个可变长度字段折叠为一个字节串,没有任何分隔符或长度前缀,使编码成为非单射的。不同的字段元组产生相同的原像、相同的哈希和相同的有效签名。
修复方案是使已签名编码成为单射的,确保任何给定字节串只能由唯一的字段元组产生。对整数使用固定宽度编码或对每个字段加长度前缀可以固定攻击者移动的边界。使用标准结构化编码器可达到相同效果且更难出错。通用规则:签署规范序列化结果而非原始拼接。这与 Solidity 中使用可变长度参数的 abi.encodePacked 属于同一类漏洞,不同输入元组可能产生相同字节序列。该问题适用于任何从可变长度字段拼接而成的已签名消息,对于构造和解析消息的双方运行在不同系统中的跨链桥尤为相关。
参考资料
- [1] BlockSec Phalcon 关于 Wanchain 漏洞利用的帖子
- [2] Wanchain Cardano-BNB Chain 跨链桥事件事后分析
- [3] Wanchain 状态 API 关于代表性 BSC 交易的记录
Lien Finance
2026 年 7 月 24 日,以太坊上的去中心化债券 OTC 协议 Lien Finance 遭到攻击,损失约 $542K 的 USDC。根本原因是债券兑换函数中的验证缺陷:它检查了输入和输出组之间共享债券的总数量是否匹配,但未追踪具体哪些债券被匹配。这使攻击者得以绕过所有输入债券的销毁步骤,同时铸造一个无抵押债券,随后通过协议的 OTC 池以真实 USDC 出售。
背景
Lien Finance 是一个针对 ETH 发行抵押债券的 DeFi 协议。其 BondMakerCollateralizedEth 合约允许用户锁定 ETH 作为抵押品来铸造债券代币。债券的收益由 fnMap 定义,这是一个将到期时抵押品价格映射到债券支付金额的分段线性函数。
有意义的单元是债券组。registerNewBondGroup() 验证组内所有债券的收益在其组合 fnMap 的每个断点处均加总为 ETH 价格。满足该条件的组恰好重构一单位抵押品。这也是组可以互换的原因:任意两个满足该条件的组价值相同,exchangeEquivalentBonds() 转换路径依赖于这一保证。
债券和组注册均无需许可:任何地址可通过提供到期时间和任意 fnMap 来注册新债券,任何地址均可将已注册债券 ID 的任意列表注册为一个组,仅受收益加总检查约束。
漏洞分析
存在漏洞的合约为 0xDA6F...BEf0 和 0x8432...7de0。
当一个债券同时出现在输入和输出组中时,销毁后立即重新铸造是无意义的操作。exceptionBonds 参数用于命名这些债券,使兑换可跳过两个步骤。函数通过单一计数器 exceptionCount 强制执行此操作:扫描输入组时每次匹配递增一次,扫描输出组时每次匹配递减一次,但从不记录哪个 bondID 发生了匹配。

这意味着输入组 [BondA, BondB] 和输出组 [BondC, BondA, BondA],以及 exceptionBonds = [BondA, BondB] 可以通过验证。计数器在输入侧达到 2(BondA 和 BondB 各匹配一次),在输出侧归零,但两次递减均来自 BondA 的两个条目。兑换不销毁任何内容,仅铸造 BondC:无需消耗任何输入即可创建输出组代币。
攻击分析
攻击分为两笔交易:步骤 1 发生在 0xe8689a...284d0f,步骤 2-5 发生在 0xb96d57...48e0e7。
-
步骤 1:攻击者三次调用无需许可的
registerNewBond(),定义了共享相同到期时间的 BondA、BondB 和 BondC。BondA 和 BondB 是在 $3,200 以下均分抵押品的杠杆看涨期权,其收益加总为ETH价格,满足_assertBondGroup()的要求。BondC 是以 $3,200 行权、当前现货价 $1,870 的深度虚值 LBT:内在价值为零,但作为期权仍有约 $7.64/IMT 的时间价值。 -
步骤 2:攻击者注册了两个债券组。
registerNewBondGroup([BondA, BondB])返回 Group 36(输入),registerNewBondGroup([BondC, BondA, BondA])返回 Group 37(输出)。BondC 的平零收益不影响断点加总,因此其存在不破坏等价条件。BondA 在 Group 37 中被列举两次,这正是使兑换通过验证的关键。 -
步骤 3:攻击者调用
exchangeEquivalentBonds(inputGroup = 36, outputGroup = 37, amount = 6968565865905, exceptionBonds = [BondA, BondB])。扫描输入组:BondA 和 BondB 各匹配一个例外,均跳过销毁,exceptionCount达到 2。扫描输出组:BondC 不匹配任何例外被铸造,BondA 的两个条目各匹配一个例外并将计数器递减至 0。 -
步骤 4:攻击者通过协议的 OTC 池(
GeneralizedDotc)出售铸造的 BondC 换取USDC,获得 532,144USDC。 -
步骤 5:攻击者对第二个
BondMakerCollateralizedEth合约重复相同模式,使总利润达到约 542,144.63USDC。

结论
exceptionBonds 机制本意是跳过共享债券的重新铸造,但通过在输出组中重复某一 bondID,可将其应用于所有输入债券。这打破了债券代币只能通过 issueNewBonds() 针对已存入抵押品创建的不变量。修复方案是追踪哪些具体 bondID 已被匹配(例如使用位图或集合),确保每个例外在两个组中各被消耗恰好一次。
关于 BlockSec
BlockSec 是一家全栈区块链安全与加密合规提供商。我们构建产品和服务,帮助客户在协议和平台的完整生命周期内进行代码审计(包括智能合约、区块链和钱包)、实时拦截攻击、分析事件、追踪非法资金,以及满足 AML/CFT 合规义务。
BlockSec 已在权威会议上发表多篇区块链安全论文,披露了若干 DeFi 应用的零日攻击,拦截了多起黑客攻击并成功救援超过 2000 万美元,保护了价值数十亿美元的加密资产。
-
官方 Twitter 账号:https://twitter.com/BlockSecTeam
-
🔗 BlockSec 审计服务 : 提交请求



