在过去一周(2026/08/17 - 2026/08/23)中,共有 2 起值得关注的重大安全事件,总损失约为 1026 万美元。
| 日期 | 事件 | 类型 | 估计损失 |
|---|---|---|---|
| 2026/08/18 | MAYAChain | 业务逻辑缺陷 | ~176 万美元 |
| 2026/08/23 | Term Finance | 治理设计缺陷 | ~850 万美元 |
选择理由
- MAYAChain:入选原因是一次链式的账务和状态验证失败,使得单笔精心构造的存款破坏了出账对账流程,并使一个低流动性资金池的记录余额被虚增,而没有真实资产支撑,这展示了若干本身各自影响有限的低层级缺陷,是如何组合起来对一个跨链流动性网络造成资金流失的。
- Term Finance:入选原因是几乎为零的治理参与率导致没有足够的投票群体来否决恶意提案,也没有守护者或撤销机制为执行延迟提供保障,使得只需极少资本的攻击者就能主导投票,通过恶意提案,并从六个协议金库中盗取约 850 万美元。
Best Security Auditor for Web3
Validate design, code, and business logic before launch
本周焦点:Term Finance
本周关注 Term Finance,是因为此次失败并非代码漏洞,而是一种治理设计缺陷——随着协议为每个金库都部署一个独立的链上 DAO,这类风险正在增加:当几乎没有人参与时,投票控制权就可以被低成本收购,金库自身的治理机制反而成为攻击面。
2026 年 8 月 23 日,以太坊上的固定利率借贷协议 Term Finance 在攻击者夺取其金库链上治理控制权后损失约 850 万美元。由于几乎没有存款人曾铸造过某个金库的治理代币,攻击者只需约 0.5 ETH 就能获得该金库绝对多数的投票权,通过协议的支持率阈值和最低参与率检查,并执行了一项恶意提案,撤销了该金库的策略并转移其资产;Term 的六个金库因此被以这种方式掏空 [1]。
背景
Term Finance 是以太坊上的一个固定利率借贷协议。Term Vaults 是在其之上叠加的一个独立产品:每个金库都是基于 Yearn V3 代码构建的 ERC-4626 金库,其中一个元金库(meta vault)接受单一资产,并将其分配到一组策略金库中。Term 的框架会在每个金库部署时同时部署一个 Aragon OSx DAO 和一个 TokenVoting 合约,而该 DAO 对随其一起发布的金库拥有升级和角色权限。因此,一个金库从诞生之初就带有自己的治理面,与谁负责策展或是否有任何资金被分配进去无关。
治理决策由 TokenVoting 执行。每个金库都有自己的份额代币(share token)和相应的 Aragon GovernanceWrappedERC20 治理包装代币;在本次分析的 ETH Meta Vault 中,这两者分别是 tmvETH 和 gtmvETH。任何份额代币持有者都可以调用 depositFor(),以 1:1 的比例获得治理代币,任何治理代币持有者都可以 delegate() 其所得投票权。当创建一项提案时,TokenVoting.createProposal() 会记录一个 snapshotBlock、一个 supportThreshold 以及基于该快照时代币供应量得出的 minVotingPower;随后,投票权会从该区块的包装代币中读取。
漏洞分析
本次事件的核心治理合约是 TokenVoting。提案执行仅由 _canExecute() 把关,对于普通(非提前)提案,该函数要求投票尚未被执行、提案已关闭,并且需要通过两项检查:isSupportThresholdReached() 和 isMinParticipationReached()。

这两项检查都是多数投票制的正确实现,但每一项衡量的都是相对比例,而不是绝对数量。isSupportThresholdReached() 仅要求赞成票数超过反对票数、且达到设定比例:

isMinParticipationReached() 要求投出的票数达到 minVotingPower,而该值本身是根据快照时的 minParticipation * totalSupply 计算得出的:

根本原因在于,这种治理设计没有为推动提案通过所需的资本量或参与广度设定任何绝对底线。这两项门槛检查都纯粹是相对于已投票数和代币供应量而言的,而由于几乎没有 tmvETH 持有者曾通过包装成 gtmvETH 的方式参与治理,该供应量以及随之而来的最低参与率门槛就几乎为零。因此,一个近乎空白的投票群体中的相对多数,就足以通过这两项检查,而由于投票者如此之少,也没有人能投出足以否决该提案的反对票。最短投票时长虽然延迟了执行,但由于没有守护者或撤销机制,这一延迟只是推迟了该提案的通过,而没有阻止它。
攻击分析
攻击者获取了某个金库治理代币的控制性份额,然后利用它通过并执行了一项将该金库资产掏空的提案;同样的手法被应用于 Term 的六个金库。以下分析基于交易 0xd354a1...d3014129 和 0x9f273f...44c2e8a0,追踪其中一个金库的情况。
-
第一步:攻击者通过 Mayan Finance 转发器,将
0.5 ETH兑换为0.485 tmvETH,该转发器完成了兑换路由,并将金库份额转给了攻击者。 -
第二步:攻击者将
0.485 tmvETH以 1:1 比例包装为0.485 gtmvETH,获得了后续步骤中所使用的投票权。 -
第三步:攻击者调用
propose(),该函数读取了攻击者自身的投票权,并调用了TokenVoting.createProposal()。合约记录了快照,读取到该区块的治理代币总供应量仅为0.535 gtmvETH,因此攻击者的持仓约占整个投票群体的 90.66%。 -
第四步:攻击者调用
vote(),将其全部投票权投给该提案,成为唯一的参与者。 -
第五步:投票期结束后,攻击者调用了
executeProposal()。门槛检查canExecute()首先检查了isSupportThresholdReached():由于攻击者是唯一的赞成票投票人,支持率远超 50% 的门槛。随后检查了isMinParticipationReached():仅攻击者自身的投票权就已超过minVotingPower。两项检查均通过。 -
第六步:该恶意提案随后被执行,撤回了
tmvETH金库分配给其策略的资金,将其重新转换为该金库持有的流动性WETH,并让攻击者提取了这些资产。此攻击手法应用于 Term 的六个金库,共造成约 850 万美元的总损失。
结论
本次事件是由治理设计缺陷而非代码错误导致的:支持率和参与率检查都是纯相对性的,因此在一个几乎空白的投票群体中,低成本获得的多数票不会遇到任何反对票,而执行延迟背后也没有守护者或撤销机制的支撑。掌控资产托管权的治理机制应当强制设定绝对的法定人数或参与率下限,并为任何执行延迟配备守护者或撤销机制及被监控的提案信息流,因为时间锁除非有人能在此期间采取行动,否则只会推迟一个已经通过的提案,而无法阻止它。
本周其他事件
MAYAChain
2026 年 8 月 18 日,基于 Cosmos-SDK 的跨链流动性网络 MAYAChain 因单笔精心构造的存款破坏了链上内部账务而损失约 176 万美元。该存款通过使有效的提款看起来像是失败了,触发了一条恢复路径,从而虚增了一个低流动性资金池所记录的原生代币余额,而没有真实资产支撑;攻击者随后通过添加和撤出流动性来提取这部分虚增的价值 [2]。经确认的链上流出资金约为 136 万美元,主要为 20.83 BTC,加上攻击者残余的原生代币持仓,官方估计总损失约为 176 万美元。
背景
MAYAChain 是一个基于 Cosmos-SDK 的跨链流动性网络,其原生资产为 CACAO。外部链上的事件由验证者观测并作为已观测交易重放到 MAYAChain 上。当一笔入账操作需要向外发送资产时,节点会调度一个或多个 TxOutItem,随后将已观测的出账交易与这些调度记录进行对账。所有资金池资产统一由共享的 Asgard 金库托管;每个流动性池只是对这一共享托管资产的一种记账仓位,而非独立分离的余额,提款则根据某个资金池的记录余额从 Asgard 中支付。
交易账户(Trade accounts)是 MAYAChain 上针对交易资产(例如 ARB~ETH 和 ARB~LINK)的原生记账仓位。交易账户的提款是通过带有诸如 trade-:ARB~LINK 备注的原生 MsgDeposit 发起的。尽管用户只签署了单笔原生交易,但存款处理器会重建一个内部的已观测交易,并以该原生交易哈希作为键存储一个 ObservedTxVoter。
漏洞分析
根本原因并非某个孤立的单一漏洞;而是一系列账务和状态验证缺陷组合叠加所致。存在缺陷的逻辑位于 MAYANode 的存款和出账处理器中。
首先,在 handler_deposit.go 中,一笔原生交易中的每条消息都会以该交易哈希为键构建一个新的 ObservedTxVoter,并通过 SetObservedTxInVoter() 保存:
txIn := ObservedTx{Tx: tx}
txInVoter := NewObservedTxVoter(txIn.Tx.ID, []ObservedTx{txIn})
txInVoter.Height = ctx.BlockHeight()
txInVoter.FinalisedHeight = ctx.BlockHeight()
txInVoter.Tx = txIn
h.mgr.Keeper().SetObservedTxInVoter(ctx, txInVoter)
由于一个批次中的每条消息共享相同的 tx.ID,后面的消息会覆盖前面消息写入的 voter 状态,从而丢弃它们的出账调度元数据。
其次,在 handler_common_outbound.go 中,出账对账从 voter.OutboundHeight(当该值为零时则使用 voter.FinalisedHeight)开始,按签名周期向前扫描:
outHeight := voter.OutboundHeight
if outHeight == 0 {
outHeight = voter.FinalisedHeight
}
for height := outHeight; height <= ctx.BlockHeight(); height += signingTransPeriod {
txOut, err = h.mgr.Keeper().GetTxOut(ctx, height)
...
}
当 voter 以这种方式被覆盖后,扫描会从最终确认的存款高度开始,永远不会检查到存有已调度出账的那个区块,因此处理器会将这些出账视为缺失,并触发惩罚性恢复路径。
第三,在 helpers.go 中,恢复路径会使用原始观测数量,将“缺失”资产按 CACAO 补贴进行估值,而没有针对该资金池实际资产深度设置任何上限:
f.stolenAsset = f.stolenAsset.Add(coin.Amount)
runeValue := pool.AssetValueInRune(coin.Amount)
f.subsidiseRune = f.subsidiseRune.Add(runeValue)
在一个仅持有约 0.11 LINK 资产侧深度的资金池中,这种无上限的转换可能将一个观测到的 LINK 数量映射成一个极其庞大的 CACAO 价值。同一处逻辑随后会在尝试转账之前,先将虚增的资金池余额提交至状态:
pool.BalanceCacao = pool.BalanceCacao.Add(f.subsidiseRune)
pool.BalanceAsset = common.SafeSub(pool.BalanceAsset, f.stolenAsset)
if err = mgr.Keeper().SetPool(ctx, pool); err != nil {
...
}
runeToAsgard := common.NewCoin(common.BaseAsset(), f.subsidiseRune)
if err = mgr.Keeper().SendFromModuleToModule(ctx, ReserveName, AsgardName, common.NewCoins(runeToAsgard)); err != nil {
ctx.Logger().Error("fail to send subsidy from bond to asgard", "error", err)
return err
}
由于该资金池的写入操作在转账之前就已提交,如果该转账无法完成资金支持,资金池的 BalanceCacao 也永远不会被回滚。最后,在 handler_observed_txout.go 中,调用方吞掉了返回的错误,将 voter 标记为已完成并继续执行,因此一种不一致的资金池状态便得以持续存在:
_, err = handler(ctx, m)
if err != nil {
ctx.Logger().Error("handler failed:", "error", err)
slashObservedOutbound("failed_outbound")
voter.SetDone()
h.mgr.Keeper().SetObservedTxOutVoter(ctx, voter)
continue
}
攻击分析
攻击者将这些缺陷组合在单笔原生交易中,随后从虚增的资金池中提取价值。以下分析基于 MAYAChain 交易 516BA14D...E9B7。
-
第一步:攻击者提交了包含 23 条消息的原生交易
516BA14D...E9B7:20 笔trade-:ARB~ETH提款、2 笔trade-:ARB~LINK提款,以及最后一笔数量为一个基本单位的DONATE:ARB.LINK。 -
第二步:交易提款消息创建了有效的调度出账,但最后的
DONATE消息覆盖了同一原生交易哈希的共享入账 voter,抹去了前面提款消息的出账调度信息。 -
第三步:当后来观测到
ARB.LINK的出账时,对账扫描使用了被覆盖的 voter 的高度字段,从未到达存有已调度出账的区块,因此将其视为缺失。 -
第四步:惩罚性恢复路径针对稀薄的
ARB.LINK资金池,为缺失的 LINK 进行估值,将资金池的CACAO侧虚增约4945 万 CACAO。原本应为该补贴提供资金的从储备金到 Asgard 的转账失败了,因为储备金仅持有约16.8 万 CACAO,远低于虚增的补贴数额;但被篡改的资金池余额依然保留下来,且失败的处理逻辑被标记为已完成。 -
第五步:在稍后的一个区块高度,攻击者向这个被扭曲的资金池添加了
100 CACAO和少量 LINK 作为流动性。由于该资金池的一侧比例被严重推高失衡,添加流动性的计算公式使攻击者获得了相对于现有约7.31 亿LP 单位而言约1 万亿的 LP 单位。 -
第六步:攻击者立即撤出了几乎全部持仓,获得了约
4887 万 CACAO和98.82 LINK,这些资金是从共享 Asgard 金库中按照虚增的资金池余额支付出来的,随后攻击者将提取到的CACAO通过 MAYAChain 资金池兑换成了 BTC 和其他资产。抛售期间,CACAO的价格从约0.115 美元跌至接近0.013 美元的低点。
攻击者提取的 4887 万 CACAO 按被利用前的价格计算名义价值超过 500 万美元,但并非全部都变现为外部收益:其中很大一部分又被兑换回资金池,使 CACAO 价格崩溃,而与攻击者无关的套利者也在这次崩盘中顺势提取了部分价值。经直接确认的链上提取金额约为 136 万美元,主要是 20.83 BTC;再加上攻击者残余的 CACAO 和交易账户余额,得出官方估计的约 176 万美元这一数字。
结论
本次事件是由一系列账务和状态验证缺陷叠加所致,而非单一漏洞:单独来看没有一步是灾难性的,但它们共同作用,让一笔精心构造的存款把一个稀薄资金池的记录余额变成了无支撑的虚增价值。核心要求是原子性和有边界的信任:一次账务变更以及为其提供资金支持的转账必须同成功或同失败,一次失败的恢复流程绝不应被标记为已完成,而针对低流动性资金池的估值必须设有上限。共享的入账状态也绝不应被同一交易中的后续消息静默覆盖。
参考资料
关于 BlockSec
BlockSec 是一家全栈式区块链安全和加密合规服务提供商。我们打造的产品和服务能够帮助客户在协议和平台的整个生命周期中执行代码审计(包括智能合约、区块链和钱包审计)、实时拦截攻击、分析事件、追踪非法资金,并履行反洗钱/反恐怖融资(AML/CFT)义务。
BlockSec 已在多个顶级学术会议上发表区块链安全相关论文,曾报告过多起 DeFi 应用的零日攻击,成功阻止多次黑客攻击并挽回超过 2000 万美元资金,并为数十亿美元的加密资产提供了安全保障。
-
官方 Twitter 账号:https://twitter.com/BlockSecTeam



