过去一周(2026/08/17 – 2026/08/23),以下 2 起值得关注的安全事件被收录,总损失约 $10.26M。下表汇总了这些事件,后续小节将展开分析。

表 1:本周收录的安全事件概览
选择理由本周将 Term Finance 列为重点,是因为这次失手不是代码 bug,而是一处治理设计缺陷——这类风险会随着协议给每个金库都配一套独立的链上 DAO 而放大:当几乎无人参与治理时,投票的控制权可以被低价买下,金库自身的治理就成了攻击面。
2026 年 8 月 23 日,以太坊上的定息借贷协议 Term Finance 因旗下金库的链上治理被接管,损失约 $8.5M。由于几乎没有存款人铸造过某个金库的治理代币,攻击者用约 0.5 ETH 就能买到该金库的超级多数投票权,通过协议的支持率与最低参与度检查,并执行一个撤销金库策略、转走金库资产的恶意提案;协议旗下共六个金库以同样方式被抽干[1]。
背景Term Finance 是以太坊上的定息借贷协议。Term Vaults 是架在它之上的一个独立产品:每个金库都是基于 Yearn V3 代码的 ERC-4626 金库,其中 meta vault 接收单一资产、并把它分配到一组 strategy vault 上。Term 的框架给每个金库都随之部署一套 Aragon OSx DAO 和一个 TokenVoting 合约,这套 DAO 对随其发布的金库握有升级与角色权限。因此,每个金库从诞生起就自带一套治理面,与谁来运营它、以及是否真有资金被分配进去都无关。
治理决策由 TokenVoting 作出。每个金库都有自己的份额代币,以及一层对应的 Aragon GovernanceWrappedERC20 治理封装;在被分析的 ETH Meta Vault 中,它们分别是 tmvETH 和 gtmvETH。任何份额代币持有者都可以调用 depositFor() 按 1:1 换得治理代币,任何治理代币持有者都可以调用 delegate() 来分配由此得到的票权。一个提案被创建时,TokenVoting.createProposal() 会记录一个 snapshotBlock、一个 supportThreshold,以及一个由该快照时刻代币总供应推导出的 minVotingPower;投票权随后在该区块从封装合约读取。
漏洞分析本次事件涉及的治理合约是 TokenVoting。提案执行只由 _canExecute() 把关:对一个普通(非提前执行)提案,它要求这张票尚未执行、提案已结束,并且两项检查通过——isSupportThresholdReached() 与 isMinParticipationReached()。

图 1:_canExecute() 把执行门槛压在 isSupportThresholdReached() 与 isMinParticipationReached() 两项检查上
这两项检查本身都是多数投票的正确实现,但它们判断的都是相对比例,而不是绝对数量。isSupportThresholdReached() 只要求赞成票按配置的比例压过反对票:

图 2:isSupportThresholdReached() 按支持率把赞成票与反对票作比较
isMinParticipationReached() 只要求已投出的票达到 minVotingPower,而 minVotingPower 本身是由快照供应推导的 minParticipation * totalSupply:

图 3:isMinParticipationReached() 把已投票数与由快照供应推导的 minVotingPower 作比较
根本原因在于:这套治理设计对推动一个提案所需的资金或参与广度没有设任何绝对下限。两项门槛衡量的都只是相对于已投票数和代币总供应的比例;而由于几乎没人参与治理把 tmvETH 封装成 gtmvETH,这个供应连同最低参与度门槛都接近于零。于是,只要占到一个近乎空置的参与者群体里的相对多数,就足以同时越过两项检查;而参与者如此之少,也根本没有人能投出足以否掉提案的反对票。提案设有最短投票时长来延迟执行,但既无 guardian 也无取消通道,这段延迟只是把已通过的提案往后拖,并不能阻止它。
攻击分析攻击者先拿到某个金库治理代币的控制份额,再用它来通过并执行掏空该金库的提案;同样的手法被施加到 Term 旗下六个金库上。以下分析追踪其中一个金库,基于交易 0xd354a1...d3014129 与 0x9f273f...44c2e8a0。
第 1 步:攻击者通过一个 Mayan Finance forwarder 把 0.5 ETH 换成 0.485 tmvETH,该 forwarder 完成 swap 并把金库份额交给攻击者。
第 2 步:攻击者把 0.485 tmvETH 按 1:1 封装成 0.485 gtmvETH,得到后续步骤所用的投票权。
第 3 步:攻击者调用 propose(),它读取自身投票权并调用 TokenVoting.createProposal()。合约记录快照时,读到的该区块治理总供应仅为 0.535 gtmvETH,因此攻击者的持仓约占整个参与者群体的 90.66%。
第 4 步:攻击者调用 vote(),把自己的全部投票权投给赞成,成为唯一的参与者。
第 5 步:投票期结束后,攻击者调用 executeProposal()。门槛函数 canExecute() 先检查 isSupportThresholdReached():由于攻击者是唯一的赞成者,支持率远超 50% 的阈值。再检查 isMinParticipationReached():攻击者一人的投票权就超过了 minVotingPower。两项都通过。
第 6 步:恶意提案执行后,把 tmvETH 金库分配到其 strategy 的资金撤回、换回金库持有的流动 WETH,并让攻击者提走资产。该手法施加于 Term 旗下六个金库,共造成约 $8.5M 损失。
结论这次事件的根源是治理设计缺陷,而非代码错误:支持率与参与度检查都只是相对判断,因此在参与者近乎为空时,低价买来的多数无人反对,执行延迟背后也没有 guardian 或取消通道。掌管资产托管的治理,应当设定绝对的法定人数或参与下限,并为任何执行延迟配上 guardian 或取消通道以及受监控的提案 feed——因为 timelock 只是拖延,除非有人能在延迟期内行动。
2026 年 8 月 18 日,Cosmos-SDK 跨链流动性网络 MAYAChain 因一笔精心构造的存款破坏了链的内部记账,损失约 $1.76M。这笔存款使有效的提现被当成失败,从而触发一条把低流动性池的原生代币余额凭空虚增的恢复路径;攻击者随后通过加、撤流动性把这批无实物支撑的价值提走[2]。链上确认的外流约 $1.36M,主要是 20.83 BTC,再算上攻击者残留的原生代币持仓,官方估计约 $1.76M。
背景MAYAChain 是一条 Cosmos-SDK 跨链流动性网络,其原生资产是 CACAO。外部链上的事件由验证者观测、并作为 observed transaction 回放进 MAYAChain。当一个入站动作需要把资产发出时,节点会调度一条或多条 TxOutItem,之后再把观测到的 outbound 交易与这些调度记录对账。所有池的资产统一托管在共享的 Asgard 金库里;每个流动性池只是这份共享托管之上的一个记账头寸,而非单独隔离的余额,提现时由 Asgard 按池的记账余额付出。
trade 账户(trade account)是 MAYAChain 上针对 ARBETH、ARBLINK 等 trade 资产的原生记账头寸。一次 trade 账户提现,是通过一笔带有 trade-:ARB~LINK 这类 memo 的原生 MsgDeposit 发起的。虽然用户只签了一笔原生交易,存款处理逻辑却会重建一个内部的 observed transaction,并存下一个以原生交易 hash 为键的 ObservedTxVoter。
漏洞分析根本原因不是某一处孤立的 bug,而是一连串记账与状态校验缺陷的叠加。有问题的逻辑位于 MAYANode 的存款与 outbound 处理逻辑中。
首先,在 handler_deposit.go 里,一笔原生交易中的每一条 message 都会以交易 hash 为键新建一个 ObservedTxVoter,并用 SetObservedTxInVoter() 存下:
由于批次里每条 message 共用同一个 tx.ID,靠后的 message 会覆盖靠前的 message 写入的 voter state,把它们的 outbound 调度元数据丢弃掉。
其次,在 handler_common_outbound.go 里,outbound 对账从 voter.OutboundHeight 起算(当它为零时改用 voter.FinalisedHeight),并按 signing period 向前扫描:
在 voter 被覆盖之后,这个扫描从 finalised 的存款高度起算,从不检查持有调度 outbound 的那个区块,于是处理逻辑把它们当成缺失、走进 slash 恢复路径。
第三,在 helpers.go 里,恢复路径用原始观测数量把这笔"缺失"资产折算成 CACAO 补贴,且完全不按池的真实资产深度封顶:
在一个资产侧深度只有约 0.11 LINK 的池里,这种未封顶的换算会把观测到的 LINK 数量折算成一个巨大的 CACAO 数值。同一段逻辑随后在尝试资金转账之前,就把这笔虚增后的池余额提交进了 state:
由于池的写入发生在转账之前,一旦转账无法获得资金,池的 BalanceCacao 也不会被回滚。最后,在 handler_observed_txout.go 里,调用方吞掉了返回的错误、把 voter 标记为 done 并继续执行,于是一份不一致的 pool state 得以留存:
攻击分析 攻击者在一笔原生交易里把这些缺陷组合起来,再从这个余额被虚增的池里提取价值。以下分析基于 MAYAChain 交易 516BA14D...E9B7。
第 1 步:攻击者提交原生交易 516BA14D...E9B7,其中含 23 条 message:20 条 trade-:ARBETH 提现、2 条 trade-:ARBLINK 提现,以及最后一条数额为一个基本单位的 DONATE:ARB.LINK。
第 2 步:这些 trade 提现创建了有效的调度 outbound,但最后那条 DONATE message 覆盖了同一原生交易 hash 对应的共享入站 voter,抹掉了前面各提现的 outbound 调度。
第 3 步:当 ARB.LINK 的 outbound 稍后被观测到时,对账扫描用的是被覆盖后的 voter 高度字段,从未到达持有调度 outbound 的那个区块,于是把它们判为缺失。
第 4 步:slash 恢复路径把这笔缺失的 LINK 拿去对流动性极低的 ARB.LINK 池估值,将池的 CACAO 侧虚增了约 49.45M CACAO。本应为补贴提供资金的 Reserve 到 Asgard 转账失败了(Reserve 只持有约 168K CACAO,远不足以支付这笔虚增出的补贴);但被改动的池余额已经落库、失败的处理逻辑也被标记为 done。
第 5 步:在稍后的一个高度,攻击者用 100 CACAO 和少量 LINK 向这个被扭曲的池加流动性。由于池的一侧已被推到严重失衡,加流动性公式给了攻击者约 1T 的 LP 份额,而此前存量份额约为 731M。
第 6 步:攻击者随即撤出几乎全部头寸,拿到约 48.87M CACAO 和 98.82 LINK(由共享 Asgard 金库按虚增后的池余额付出),并把提出的 CACAO 通过 MAYAChain 的池换成 BTC 等资产。抛售期间 CACAO 价格从约 $0.115 跌到接近 $0.013 的低点。
攻击者提走的 48.87M CACAO 按事发前价格名义上价值超过 $5M,但并未全部变现为外部收益:其中大部分被换回池中、把 CACAO 价格砸崩,且与攻击者无关的套利者也在崩盘中抽走了价值。直接确认的链上提取约为 $1.36M,主要是 20.83 BTC;再算上攻击者残留的 CACAO 与 trade 账户余额,数字接近官方估计的约 $1.76M。
结论这次事件源于一连串记账与状态校验缺陷,而非单一的 bug:每一步单看都不致命,但叠加起来,就让一笔精心构造的存款把一个浅池的记账余额变成了无实物支撑的价值。核心要求是原子性与有界信任:记账变更与为其提供资金的转账必须同成同败,失败的恢复路径绝不能被标记为 done,对低流动性池的估值也必须封顶。此外,同一笔交易中靠后的 message 绝不应静默覆盖共享的入站状态。
[1] RockawayX — Post Mortem: Term Finance Governance Incident,https://x.com/RockawayX/status/2091503724795474387
[2] MAYAChain incident analysis,https://x.com/AaluxxMyth/status/2089792721086996703