返回博客

损失约2300万美元:Cosmos EVM与Moonwell遭利用攻击 | BlockSec Weekly

Code Auditing
2026年9月3日
阅读约 29 分钟
核心要点
  • 本周发生的五起事件在以太坊(Ethereum)、Solana、Base、Cronos以及另外六条 Cosmos EVM 链(如 TAC)上造成约2270万美元的损失。

  • 共享代码使两起事件演变为多协议事件。Cosmos EVM 余额处理漏洞在同一周内影响了六条 Cosmos EVM 链,其中包括 TAC Chain;Rain 过时的卡片合约暴露了 Avici、Tria 及其他卡片程序。通用基础设施中的一个缺陷可以波及运行该基础设施的每一个易受攻击的下游系统。

  • 价格操纵是最常见的攻击类型,占五起事件中的两起,并造成了本周大部分损失。Moonwell 和 Tectonic 的漏洞攻击在 Compound 式市场上采取了相同的手法:每次都同时抬高了预言机价格和市场凭证代币的兑换比率,然后利用被高估的抵押品进行借贷。

在报告期内(2026/08/22 - 2026/08/30),我们观察到5起安全事件,估计总损失约为2270万美元。

日期 事件 类型 估计损失
2026/08/20* The Cosmos EVM Exploit Series(MANTRA、TAC、KiiChain,+3) 算术下溢/上溢 ~570万美元*
2026/08/27 Moonwell 价格操纵 ~910万美元
2026/08/28 Ajna 不当业务逻辑 ~77.5万美元
2026/08/28 Rain Card Contract Exploit Series(Avici、Tria 等) 签名验证绕过 ~110万美元†
2026/08/30 Tectonic 价格操纵 ~600万美元‡

*Cosmos EVM 系列事件始于08/20(MANTRA),发生在本周报告期之前,未在上周报告中涉及;为完整起见,本报告将其纳入。根据Cosmos官方事后分析报告,按8月19日的价格计算,攻击者在受影响的六条链上共变现约570万美元(约287万美元通过DEX、约285万美元通过中心化交易所,此后已被冻结)。在已知代币数量的情况下,名义提取金额更大(TAC约30亿枚TAC代币,约750万美元;MANTRA 7.209亿枚代币,约360万美元;KiiChain 1.483亿枚KII代币),但大部分代币未被出售、已被冻结或可在链上追回。

†约110万美元是通过共享合约暴露的Rain支持项目中估计的总损失,Avici和Tria是其中最大的两起,分别披露约500,859美元(1,685名用户)和431,945美元(636名用户)的损失。

‡约600万美元是回滚前已跨链至以太坊的实现损失。总提取金额估计范围从约7400万美元(在攻击者钱包中追踪到的金额)到约1.195亿美元(市场净流出总额)不等;大部分资金留在Cronos链上,并在验证者将链回滚至攻击前状态时被清除。Tectonic和Cronos均未确认最终损失数字。

Web3领域最佳安全审计机构

在上线前验证设计、代码和业务逻辑

本周焦点:The Cosmos EVM Exploit Series(以TAC链为例进行追踪)

选取此系列事件是因为它揭示了共享基础设施在披露方面的问题:由于该漏洞被误判为不构成严重安全威胁,其修复以静默公开补丁的形式发布,而非通过协调一致的私下分发方式;随后第三方分叉公开描述了该攻击路径,使所有仍在运行该模块且未打补丁的链都暴露在风险之中。

在2026/08/20至08/25期间,攻击者利用一条单一的攻击链,将六条Cosmos EVM链上共享的cosmos/evm模块中的两个漏洞组合利用。这两个漏洞都存在于协调EVM状态与Cosmos x/bank账本的代码中:一个是余额下溢,另一个是与之匹配的上溢,二者被串联在一笔供应中性的交易中。

根据Cosmos官方事后分析报告,该漏洞于4月通过漏洞赏金计划报告,最初被误判为不会威胁生产资金,因此走了静默公开补丁流程,并于08/19在v0.6.2和v0.7.2版本中发布。08/20,第三方分叉公开描述了该攻击路径,几小时后首次资金被盗取,首先波及MANTRA(08/20),随后是TAC和KiiChain(08/22)[1][2]。在全部六条链上,按8月19日的价格计算,攻击者共变现约570万美元(约287万美元通过DEX售出,约285万美元通过中心化交易所出售,此后已被冻结)[1]

本报告详细分析TAC链作为具体案例;它是该系列事件中受损最严重的链,质押池名义损失约750万美元[3]

背景

TAC链同时运行Cosmos SDK和EVM。一个tac1...地址和一个0x...地址共享相同的底层20字节,因此同一个地址可以先被创建为Cosmos归属(vesting)账户,之后再在其上部署EVM合约。这并非两个独立的账户:同一个地址同时携带归属状态和合约代码。二者并行运行,TAC通过固定地址上的预编译合约将质押等原生Cosmos操作暴露给EVM调用者,因此EVM合约可以像调用其他普通调用一样调用它们。

归属账户的Bank总余额包括锁定的代币。锁定部分在归属完成前不能转账,而可支配余额是从总余额中减去锁定金额后剩余的部分。当EVM加载账户时,会将StateDB余额初始化为可支配余额,且执行期间的合约转账操作是基于该余额进行的。

在一笔交易结束时,StateDB中的余额变化会结算回x/bank这一权威账本,只有在那里结算的部分才是真实的、可转账的TAC。TAC运行的是v0.7.x系列版本,该版本会将EVM余额直接写入x/bank,但前提是该值能通过uint256int256的转换,因此接近2^256的余额根本无法被结算。

锁定的代币无法转账,但仍可用于质押委托。委托检查的是Bank总余额(其中包含锁定代币),随后通过DelegatedVesting保留归属限制。对于一个total=1的完全锁定账户,委托该单位金额会将Bank总余额减为0并更新DelegatedVesting,而可支配余额在委托前后都正确地保持为0

漏洞分析

存在缺陷的组件是共享cosmos/evm处理器中的余额同步逻辑,该处理器在每次有状态的质押预编译调用时都会运行,通过位于0x0000...0800的质押预编译暴露出来,并在[4]中实现。它通过未经检查的uint256算术运算来协调EVM余额与Cosmos账本,这暴露出两个相互补充的缺陷:一个是质押回写路径上的余额下溢,另一个是普通加法路径上与之匹配的上溢。二者单独存在时都不危险;风险来自它们在同一个共享算术路径上的组合。

第一个缺陷是余额回写时的下溢。 当一次有状态的质押预编译调用执行时,原生Cosmos操作会在BeforeBalanceChangeAfterBalanceChange两个钩子之间运行,AfterBalanceChange负责将原生余额的变化反映回EVM StateDB。

委托操作是针对Bank总余额进行验证的,因此一个完全锁定的账户可以通过委托检查,而其可支配余额仍保持为0。原生委托操作会从Bank总余额中扣除委托金额,并触发一个coin_spent事件。缺陷在于AfterBalanceChange消费该事件的方式:它没有重新读取账户当前的可支配余额并进行赋值,而是重放了coin_spent金额,执行stateDB.SubBalance(spender, amount),从现有的EVM余额中进行扣减。

由于SubBalance是用未经检查的uint256算术执行减法运算的,任何EVM余额小于事件金额的账户都会发生下溢。对于EVM余额为0coin_spent金额为1的账户,0 - 1会环绕成为MAX_UINT256

第二个缺陷是加法路径上与之匹配的上溢。 每一次余额转账都会通过同一个StateDB对发送方执行SubBalance、对接收方执行AddBalance,因此两个方向共享同一条算术路径。

两者最终都归结到相同的stateObject基础操作,其中AddBalanceSubBalance分别计算new(uint256.Int).Add(s.Balance(), amount).Sub(s.Balance(), amount),且没有任何范围检查。正如减法会在0以下发生下溢一样,一次足够大的加法也会在MAX_UINT256以上发生上溢并环绕回小数值。

这两个缺陷是相辅相成的。单独来看,下溢产生的MAX_UINT256余额是无效的,因为接近2^256的数值无法通过结算到x/bank所需的uint256int256转换。而未经检查的加法正是与之配对的部分:它是唯一能将如此巨大的余额带回到该结算上限以下的路径。

攻击分析

以下分析基于交易0xae4e9b...da46fc

  • 步骤1:在交易0x4da591...df1af7中,攻击者部署了一个CREATE2工厂合约,以便在实际部署攻击合约之前,就可以计算出攻击合约地址0x5711...c978

  • 步骤2:在交易95F43742...6A885BA中,攻击者使用MsgCreateVestingAccount向该未来的合约地址转账并锁定一个基本单位(1utac),使其获得能通过委托检查的Bank总余额,同时使其可支配余额保持为0

  • 步骤3:在交易0x2400f8...c57c81中,攻击者使用CREATE2将攻击合约部署到同一个0x5711...c978地址,使该地址同时成为一个Cosmos归属账户和一个EVM合约,从而使外部账户可以支付gas费用,同时该合约地址仍作为可支配余额为0的委托人。
  • 步骤4:攻击合约通过质押预编译委托了那个锁定的基本单位。质押流程接受了针对Bank总余额的这笔委托,随后的余额回写操作使该合约的EVM余额环绕成为MAX_UINT256

  • 步骤5:环绕产生的MAX_UINT256余额无法原样结算回Cosmos账本,因此攻击合约首先将其降至一个可结算的数值。它向bonded_tokens_pool(该链上持有全部质押TAC、余额最大的账户)转出了几乎全部余额,选择的金额使该资金池的余额在加法运算中发生上溢并环绕为0。由于该金额是从攻击合约自身的MAX_UINT256中扣除的,攻击合约最终恰好持有该资金池原先的全部余额,没有产生新的供应量。将该资金池清零可以获取其全部余额,这是这种上溢所能带来的最大收益,这也是攻击者一开始就选择这条链上最大账户作为目标的原因。

  • 步骤6:随后攻击合约将该余额,即2,985,651,403.40枚TAC,转给了攻击者的地址。

我们在交易层面的分析结果与Cosmos官方事后分析报告一致,该报告描述了两个链式漏洞:上文所述的下溢产生了异常余额,而攻击分析步骤5中的上溢则将其转化为资金池的真实资金[1]。在该官方报告发布之前发布的一份早期KiiChain事后分析报告认为,此次事件涉及至少三个上游缺陷,且目前只有下溢漏洞被修复[2]。独立的媒体报道也总结了关于究竟还有多少上游缺陷尚未解决的这种分歧观点[5]

结论

The Cosmos EVM Exploit Series事件的根本原因在于,EVM层与Cosmos层在核算同一笔余额时存在不一致,再加上从未经过边界检查的算术运算。当两个运行时共享同一个账本时,它们必须在每一个具体账户层面上就余额语义达成一致,并且每一次余额变化都必须检查是否存在上溢和下溢。由于该缺陷存在于共享模块中,而非某条链自身的代码中,一个漏洞就使所有运行该模块的链都暴露在风险之中,这正是一个单一漏洞演变成多链事件的原因。除代码层面外,此次事件也是一次关于严重性评估的教训。该漏洞最初被判定为不会威胁生产资金,因此其修复以静默公开补丁的形式发布;等到这一判断被纠正时,该补丁已经公开,第三方对攻击路径的披露则将这一误判转化为六条链遭到攻击的现实。能够转移真实资金的共享基础设施漏洞,从一开始就需要私下、协调一致的分发方式,而不能是任何人都可以解读的静默公开补丁。

开始使用Phalcon Explorer

深入交易细节,做出明智决策

立即免费试用

本周更多事件


Moonwell

2026/08/27,Base链上的Moonwell遭到攻击,损失约910万美元,攻击手法是将抵押品估值虚增与对MAMO(其Core Market中一个低流动性资产)的预言机价格操纵相结合。除了推高MAMO价格外,攻击者还直接将MAMO转入mMAMO市场合约而不铸造份额,这提高了每份份额所对应的支撑资产,在价格上涨的基础上进一步虚增了抵押品价值。基于这种双重虚增的抵押品,攻击者在cbBTCWETHUSDCwstETH等资产上共借出约1103万美元的总借款,在清算后留下约913万美元的剩余债务[6][7]

漏洞分析

Moonwell的Core Market运行在Compound v2代码之上,其中一份市场份额(mMAMO)的价值为(cash + totalBorrows - totalReserves) / totalSupply。这里存在两个相互结合的弱点。首先,MAMO的供应上限只检查正式的铸造路径,因此直接将MAMO转入mMAMO合约会增加该市场的现金储备而不会铸造份额;这会提高计算出的兑换率(exchangeRateStored()),从而提升所有现存份额的抵押品价值,同时完全绕过了供应上限。其次,尽管流动性稀薄,MAMO仍被列为抵押品,且抵押系数高达50%,因此其预言机价格可以用不大的资金量来撬动。由于抵押品价值的计算方式是份额数量乘以兑换率再乘以预言机价格,兑换率和价格都是可被操纵的因素,将二者同时虚增会成倍放大针对流动性更强资产的借贷能力。

攻击分析

以下分析基于交易0x09687d...395593e。此次操作的启动资金约为194.7万美元(799枚ETH兑换为USDC并跨链至Base);在借入的资产被循环利用后,MAMO的总购买量达到约750万美元。

  • 步骤1:攻击者先通过正规方式提供MAMO以铸造mMAMO,随后分两笔交易将53,393,290枚MAMO直接转入mMAMO合约,这两笔交易均未触发Mint事件,将该市场的计算兑换率提高了约3.68倍,从而重新估值了攻击者刚刚铸造的mMAMO份额以及其他所有持有者的份额。

  • 步骤2:在流动性稀薄的情况下,攻击者在多个DEX资金池中买入MAMO,将MAMO/USD价格从约0.0106美元推高至约0.43美元

  • 步骤3:在兑换率和价格都被虚增的情况下,攻击者完成了18笔跨cbBTCWETHUSDCwstETH的借款(总计约1103万美元),随后转换并汇集了这些收益,通过CCTP将约872.9万枚USDC跨链至以太坊,并兑换成约872.8万枚DAI

结论

根本原因在于,Moonwell同时在两个可独立操纵的层面上对一种低流动性的抵押资产进行估值:一是其预言机价格,稀薄的流动性使攻击者能够用不大的资金撬动它;二是其凭证代币的兑换率,标的资产的直接转入会使兑换率虚增,因为供应上限只防护了正式的铸造路径。由于抵押品价值是这两者的乘积,同时虚增二者会成倍放大针对流动性更强资产的借贷能力。借贷市场应将抵押品的价格和份额核算方式都视为可能被操纵的对象:将未经请求的转账排除在兑换率计算之外,并对低流动性资产采取保守的抵押系数,配合具有流动性感知能力的定价方式以及严格的供应与借款上限。

Ajna

2026/08/28,Ajna在七个以太坊资金池中遭到攻击,损失约77.5万美元,攻击利用了其清算路径中的一个业务逻辑缺陷。Ajna是一个不使用外部预言机的借贷协议;它通过从资金桶(bucket)流动性推导出的LUP(最低已使用价格,Lowest Utilized Price)为头寸定价。攻击者首先操纵LUP,制造出一个严重资不抵债的头寸,随后促使协议以协议此时仍持有的、远高于市场价的荷兰式拍卖价格清算一个受其控制的头寸,从而使资金桶的存款按面值以计价代币被消耗以偿还债务,而攻击者则因此获得了真实的抵押品[8]

背景

Ajna是一个无需许可的协议,任何人都可以创建一个配对代币资金池用于存借款,类似于Uniswap的资金池。每个资金池都被划分为若干个buckets(资金桶),类似于Uniswap V3中的价格刻度,每个刻度代表每单位抵押品对应的固定数量计价代币。放贷人根据自己偏好的贷款价值比选择一个资金桶并在其中提供流动性,作为回报获得LP(对该资金桶存款的一种索取权)。由于没有外部预言机,LUP是直接从资金桶的存款分布和总债务中推导出来的。

一旦某个头寸的threshold price(其债务除以其抵押品)高于LUP,该头寸即可被清算。此时任何人都可以通过提交保证金将其kick(踢入)清算,从而开启一场荷兰式拍卖。拍卖价格与现货市场并不挂钩:它以参考价格的32倍起拍,每小时减半一次,且在最初一小时内(一个宽限期)保持不变,之后才开始衰减。

被拍卖的抵押品可以通过两种方式获取。调用者可以通过take()按当前拍卖价格支付计价代币来获取抵押品,也可以调用bucketTake(),后者会使用某个选定资金桶中的计价代币存款来购买被拍卖的抵押品并偿还借款人的债务。在bucketTake()中,获得的抵押品会计入该资金桶,因此获得该抵押品的是该资金桶的放贷人;对于套利型的资金桶清算(arbitrage bucket take),调用者还会额外获得LP奖励(对该资金桶的一种索取权),奖励金额基于资金桶价格与拍卖价格之间的价差,以此作为触发清算的激励。这一设计假设,只有当拍卖价格已下跌至该抵押品的实际价值或以下时,接盘者才会介入:在没有预言机的情况下,这种降价拍卖正是Ajna发现公允价格的方式,而某个资金桶的放贷人则通过以低于自身资金桶价格的价位获取抵押品来获利。拍卖本身只有在借款人的债务被清偿完毕或该借款重新达到足额抵押状态时才会结束;另有一条独立的结算路径会在72小时宽限期结束后,或在抵押品耗尽后提前,对拍卖进行结算,吸收任何剩余坏账,并将剩余抵押品返还给借款人。

这一机制中的三个实现细节决定了一次清算操作所产生的具体数值。首先,拍卖价格并非由市场决定;它按32 * max(kickMomp, neutralPrice) * 2^(-max(elapsedHours - 1, 0))衰减,因此在第一个小时的宽限期内保持不变,此后才开始下降,并在宽限期刚结束后仍接近其参考价格的32倍。

其次,只有当借款人在计入惩罚之前的债务下已经处于抵押不足状态时,一次kick操作才会被接受:_kick()会先运行_isCollateralized()检查,若未通过则以BorrowerOk()回滚,之后才会加上三个月利息的惩罚,因此该惩罚只会提高清算后记录的债务,而并未帮助该头寸满足清算资格。

第三,对一场拍卖的首次take操作会在计算该次take的偿还金额和抵押品数量之前,先给借款人的债务加上7%的惩罚,因此单笔bucketTake()操作不一定能独自清偿全部债务。

漏洞分析

存在缺陷的合约是Ajna资金池合约(0xad24...178e);本文所述的清算逻辑取自该已验证的源代码。在bucketTake()内部,协议以计价代币的面值使用某个资金桶的存款索取权来偿还拍卖借款人的债务,并且在一次套利型资金桶清算中,会根据资金桶价格与拍卖价格之间的价差向调用者支付LP奖励。

它从未检查这些存款索取权的实际可回收价值,也未检查拍卖价格在经济上是否合理;它只是假设这些索取权日后可以按面值兑现。它所依赖的两个价格是各自独立计算的,且彼此之间没有任何相互校验:LUP跟随资金桶存款分布和资金池总债务变化,而拍卖价格则纯粹根据自kick时固定的参考价格随经过时间衰减。因此,某个索取权的链上面值可能与其实际可回收的价值不同,而bucketTake()仍然按照该面值来结算债务。

在内部,bucketTake()通过_takeBucket()进入_rewardBucketTake(),在此,在一次套利型清算中,调用者的LP奖励被计算为所获取的抵押品数量乘以资金桶价格与拍卖价格之间的价差。

攻击分析

以下内容基于对cbETH资金池(受影响的七个资金池之一)的链上分析,使用了交易0x8a8793...016e640x12dfde...14e4f5

  • 步骤1:在准备交易中,攻击者向索引为2000(一个远高于市场价的价格档位)的资金桶添加了约49.343枚WETH的计价代币流动性。以这个虚高的资金桶作为支撑,攻击者开立了一个几乎无抵押的头寸,质押约0.001枚cbETH借出约49.319枚WETH。该头寸几乎不持有任何抵押品,因此并非攻击目标本身;它起到两个准备作用。第一,作为一笔资不抵债的债务,一旦市场恢复正常,它会拉低LUP。第二,由于存入2000号资金桶的49.343枚WETH中几乎全部已被借出,仅剩少量余额,2000号资金桶留下的存款索取权其面值(约49.343枚WETH)远超其实际可回收的价值;这一受损的索取权正是攻击者随后在步骤4中按面值消耗的“弹药”。
  • 步骤2:在同一笔准备交易中,攻击者使用第二个受控地址0x02d329...6f5f,在正常价格水平上开立了一个借款人头寸,质押约48.128枚cbETH作为抵押品,并据此借出约49.319枚WETH。这使市场价格回到了正常区间,导致第一个头寸严重资不抵债,而第二个头寸则恰好处于其健康阈值附近。这第二个持有真实抵押品的头寸,才是攻击者实际打算掏空的目标;那个资不抵债的第一个头寸只是撬动LUP的杠杆。

  • 步骤3:随后攻击者将第二个头寸(由0x02d329...6f5f开立的那个)踢入清算拍卖,而非第一个资不抵债的头寸。只有当某个头寸在计入惩罚之前的债务相对于当前LUP已经处于抵押不足状态时,kick操作才会被接受,而此时资不抵债的第一个头寸已将LUP拉低到足以使第二个头寸累计的债务满足该标准的程度。只有在通过资格检查之后,协议才会加上三个月利息的kick惩罚,这也是清算后记录的债务达到约49.362枚WETH的原因。此次kick操作开启了该头寸的荷兰式拍卖。

  • 步骤4:攻击者一直等到一小时宽限期刚结束时,此时拍卖价格才刚刚开始衰减,仍接近参考价格的32倍。攻击者在2000号资金桶(正是当前持有受损索取权的那个资金桶)上调用bucketTake(),以这一仍然虚高、远超市场价的价格清算了该头寸。由于这是该拍卖的首次take操作,协议在计算该次take的偿还金额和抵押品数量之前,先向借款人的债务加上了7%的惩罚。抵押品是按拍卖价格计价的,因此消耗约49.34枚WETH2000号资金桶存款只偿还了该债务的一部分,并只移除了约1.51枚cbETH的抵押品,使大部分抵押品未受影响,而调用者则借此获得了基于该价差的大量LP奖励。
  • 步骤5:攻击者通过removeCollateral()赎回了这些LP奖励,将部分利润以抵押品的形式提取出来。

  • 步骤6:为了收尾,攻击者获取了一笔Balancer闪电贷,并调用Ajna的take()来偿还bucketTake()留下的剩余债务,这一次是用真实的计价代币偿还,直到借款人的债务归零、拍卖退出。随后一次repayDebt()调用提取出了此时已不再受限的抵押品,约46.51枚cbETH,其中quoteRepaid=0:没有为此归还任何计价代币。这笔收益是以资金池为代价获得的。这一亏空并未消失,而是转移了:随着第二个头寸的关闭,第一个头寸那笔仍未偿还、几乎没有支撑的贷款作为坏账留在了资金池中,由其他放贷人承担。

结论

根本原因在于,Ajna的清算路径是按面值使用资金桶的存款索取权来结算拍卖借款人的债务,而没有检查其实际可回收价值,也没有检查拍卖价格在经济上是否合理。正是这一缺失的检查,使得一个被人为制造出来的受损索取权得以按面值被消耗,从而掏空真实抵押品,将亏空留在资金池中作为坏账。清算路径应当按照存款索取权的实际可回收金额而非面值来对其估值,在完成一次take操作之前确认拍卖价格在经济上是否合理,并防范消耗那些已被资金池现有坏账所损害的索取权。受影响的资金池合约是不可变的,也没有提供任何管理暂停功能,因此一旦掏空开始,就没有任何办法可以阻止它;用户只能自行撤出资金。

The Rain Card Contract Exploit Series(以Avici为例进行追踪)

2026/08/28(UTC),Rain共享的Solana卡片抵押品程序的一个过时版本因Ed25519签名验证绕过漏洞而遭到攻击。攻击者通过使该程序接受一个伪造的管理员批准,从而获得了对用户抵押品账户的控制权,并掏空了其中持有的代币余额。由于该缺陷存在于跨多个Rain驱动的卡片程序共享的程序代码中,一个漏洞就使所有这些程序同时暴露在风险之中:此次攻击共造成约110万美元的估计总损失,其中Avici和Tria是最大的两起,分别披露损失约500,859美元(1,685名用户)和431,945美元(636名用户)[9]

以下分析以Avici为具体案例。

背景

在Solana上,签名检查是作为交易处理的一部分由原生Ed25519预编译程序执行的:如果所引用的签名无效,整笔交易都会失败。业务程序并不会直接收到该检查结果;它会检查同一笔交易中的其他指令(通过Instructions系统变量),并依赖于该验证已经通过这一假设。当用户为一张Rain驱动的卡片充值时(如Avici的情况),其余额会保存在由共享Rain程序管理的每用户抵押品账户中,该账户的代币权限来源于该程序,因此该账户的管理员可以通过该程序的转账流程移动该账户中的资产。

更改某个抵押品账户的管理员需要两个签名。其中一个必须来自协议指定的管理员;另一个签名者则没有特殊的身份要求。这一设计依赖于链下授权:一旦协议管理员对更改管理员的消息签了名,该程序就会将其视为已获批准。

一条Ed25519验证指令以一个字节的待检查签名数量和一个填充字节开头,之后是每个签名对应的一个Ed25519SignatureOffsets结构体。每个结构体不仅指定了签名、公钥和消息的字节偏移量,还指定了应从哪条指令索引中读取这些内容[10]。这些索引是一项有意设计的功能:它们使得一次验证可以从该交易中任意被索引的指令数据中读取其输入,例如可以在不复制该指令数据的情况下,验证一个签名是否是针对另一条指令的数据签署的。原生验证器只是简单地读取这些偏移量和指令索引所指向的内容。

漏洞分析

该缺陷存在于抵押品程序(3zVB...yBzDuc)中:它信任一个公钥,却没有确认该公钥就是验证器实际检查过的那一个。为了记录这个批准管理员变更的签名者,该程序会从其所检查的Ed25519验证指令内部的一个固定位置读取一个公钥,然后仅凭该验证通过这一事实,就断定该公钥的持有者签署了这条更改管理员的消息。

它从未确认过该验证实际检查的对象究竟是什么。由于这些指令索引字段是由调用者控制的,而运行时会将每条指令的数据都交给原生验证器,因此一条验证指令可以在自身的数据体中携带一个公钥,同时其指令索引却将验证器引导到另一条指令上,从而在一条来源不同的消息上验证一个由不同公钥所签署的签名。

因此存在两处对“管理员公钥”的读取,二者之间没有任何绑定关系:该程序信任的是它所检查的指令中存放的那个公钥,而验证器实际检查的却始终是索引所指向的内容。管理员的公钥可以作为数据出现在该交易中,即便从未有任何在该公钥下有效的签名被真正验证过。这种脱节正是该漏洞所在,而缺失的正是那项本应强制要求验证器实际验证过的公钥和消息,与程序所信任的公钥和消息保持一致的检查。

攻击分析

以下分析基于交易ZmpBgn...mqWL

  • 步骤1:攻击者构造了一笔包含两条Ed25519验证指令、后跟SubmitSignatures的交易。指令0携带了攻击者自己的公钥(cafa…53db)以及针对更改管理员消息的一个真实签名,使该交易拥有一次真正有效的Ed25519验证。
  • 步骤2:指令1将协议管理员的公钥(a2fc…959a)放入公钥槽位,但在签名槽位中填入了伪造的0x09字节,因此如果真的对这条指令自身的数据进行验证,该指令本应失败。

  • 步骤3:攻击者将指令1的Ed25519头部设置为01003000000010000000700020000000。只有三个指令索引字段起到了重定向作用,解码后它们的值均为零,因此签名、公钥和消息全都从指令0中读取(字节偏移量仍指向该指令数据内部):

    num_signatures      = 1,    padding = 0
    signature_offset    = 48,   signature_instruction_index   = 0
    public_key_offset   = 16,   public_key_instruction_index  = 0
    message_data_offset = 112,  message_data_size = 32,  message_instruction_index = 0

    因此第二次验证实际重新读取了指令0,重新验证了攻击者自己的有效签名,而不是指令1中那个无效的0x09负载。

  • 步骤4:攻击者调用了SubmitSignatures。该程序看到了两次成功的Ed25519验证,但在记录第二个签名者时,它读取的是嵌入在指令1中的协议管理员公钥,因此它将该管理员公钥当作一个已获批准的第二签名者予以接受。

  • 步骤5:在这一虚假批准被记录之后,攻击者利用更改管理员的流程,将受害者抵押品账户的管理员设置为自己,从而将这一签名验证绕过转化为对该账户的直接控制权。

  • 步骤6:在将调用者验证为该抵押品的管理员之后,该程序通过其转账流程,以自身的程序派生地址(PDA)作为该代币账户的权限方,调用转账操作,将资产从用户的卡片抵押品代币账户中转出。

结论

The Rain Card Contract Exploit Series事件的根本原因在于,该抵押品程序确认了一次原生Ed25519验证已经运行,但从未确认它所信任的公钥和消息就是验证器实际检查过的那些。任何依赖原生签名验证的程序,都必须将其所使用的授权数据与该验证绑定:要么要求一种自包含的Ed25519数据布局,使其输入只能从当前指令中读取;要么就对每一条被引用的指令进行解析,并逐字节比对验证器实际验证过的公钥和消息,与程序据以采取行动的数据是否一致。同一个漏洞在众多Rain驱动的卡片程序中长期未被修复运行,这正是使一个单一漏洞演变成波及多个程序的事件的原因。

Tectonic

2026/08/30,Cronos链上一个采用Compound式架构的借贷协议Tectonic遭到攻击,原因在于其低流动性治理代币TONIC被允许作为抵押品,且抵押系数高达20%。攻击者同时在两个层面上虚增以TONIC计价的抵押品:一方面通过直接转账操纵tTONIC兑换率,另一方面通过DEX买入推高预言机价格。基于这种双重虚增的抵押品,攻击者在多个借贷市场进行了借款。约629万美元(2,592枚ETH)被跨链至以太坊,构成了实现损失;掏空资金的大部分留在了Cronos链上,并在验证者将该链回滚至攻击前状态时被清除。Tectonic和Cronos均未确认最终损失数字[11]

漏洞分析

根本原因在于将低流动性的治理代币TONIC列为抵押品,且抵押系数高达20%。尽管TONIC具有治理属性,但其链上流动性极为稀薄,因此其估值可以用有限的资金大幅撬动。与Moonwell的攻击类似,此处也存在两个可同时被操纵的层面:TONIC/USD预言机价格,以及tTONIC凭证代币的兑换率——向该市场直接转入TONIC会提升该兑换率,而不会抵消相应的债务[11]。任何非零的抵押系数随后都会将这种被虚增的估值转化为针对流动性更强资产的借贷能力。

攻击分析

以下攻击过程的重建基于链上情报以及一份详尽的归档节点重建分析[11][12]

  • 步骤1:攻击者向Tectonic提供了约500万枚USDC作为抵押品。当时TONIC/USD预言机价格仍将该代币估值在约0.0000000106美元附近,一个受控账户借出了约376.54万亿枚TONIC并转移至第二个账户。

  • 步骤2:第二个账户通过正规方式提供了约41.87万亿枚TONIC,作为回报获得了tTONIC(该市场的凭证代币)。

  • 步骤3:攻击者将剩余借入的TONIC中的大部分直接转入tTONIC市场合约,而没有偿还最初借入的TONIC债务,从而提高了tTONIC兑换率,提升了第二个账户所持tTONIC的抵押品价值。

  • 步骤4:利用被虚增的tTONIC抵押品,攻击者借出20万枚USDC和约696万枚CRO,随后在TONIC/USDCTONIC/WCROTONIC/VVS资金池中买入约16.23万亿枚TONIC,并将其重新投入该市场。这进一步提高了兑换率,并推高了DEX现货价格;Tectonic的链下TONIC/USD价格源随后接受了一系列迅速上涨的报价(从12:19 UTC时的约0.0000000106美元上涨到12:49 UTC时的约0.00000208美元)。这是外部层面的操纵。

  • 步骤5:攻击者又借出约331万枚USDC和2121万枚CRO,用以买入另外7.68万亿枚TONIC,并再次将其投入该市场,从而在提取资金之前进一步强化了这种被虚增的兑换率和被操纵的价格。

  • 步骤6:在最后一笔交易中,攻击者利用这种双重虚增的抵押品,在多个借贷市场进行提款,共提取了约5524万枚USDC、4565万枚USDT、98枚WBTC、1,895枚WETH、1675万枚CRO以及其他资产。约629万美元(约2,592枚ETH)在验证者暂停该链之前被跨链至以太坊。区块生产于2026/08/30 23:49 UTC恢复(于08/31宣布),此前验证者将链状态回滚至攻击前的一个区块,从而清除了Cronos链上的余额;只有已跨链的约629万美元构成实现损失。

结论

与本周早些时候的Moonwell攻击事件类似,这也是一起针对Compound式市场的价格操纵攻击,该市场接受了一种低流动性代币作为抵押品,并同时在两个层面虚增该抵押品的价值:预言机价格和凭证代币的兑换率。借贷协议应避免将低流动性资产列为抵押品,在必须支持此类资产时应实施严格的供应与借款上限,并采用时间加权或具有流动性感知能力的定价方式,使稀薄的现货市场无法撬动预言机。兑换率这一层面也需要单独设防:某个市场的抵押品核算应将标的资产未经请求的转账排除在外,使直接转账无法在相应债务仍未偿还的情况下虚增凭证代币的兑换率。对异常价格和借贷活动的链上监控,能够进一步缩短资金被跨链转出之前的响应时间窗口。

开始使用Phalcon Security

检测每一个威胁,只提示重要信息,并拦截攻击。

立即免费试用

参考资料

关于BlockSec

BlockSec是一家全栈区块链安全与加密合规服务提供商。我们打造各类产品与服务,帮助客户在协议和平台的整个生命周期中进行代码审计(涵盖智能合约、区块链及钱包)、实时拦截攻击、分析事件、追踪非法资金,并满足反洗钱/反恐怖融资(AML/CFT)合规义务。

BlockSec已在多个知名会议上发表了多篇区块链安全论文,报告了多起DeFi应用的零日攻击,成功拦截多起黑客攻击,挽回超过2000万美元的损失,并保障了数十亿美元加密资产的安全。

订阅最新动态
Web3攻击面:渗透测试概览

Web3攻击面:渗透测试概览

加密机构承载所有传统攻击面,并叠加资金处理链。本文提供系统的实用抽象:应用、授权与签名、区块链交互、基础设施四组件模型,说明各自职责、代表实现及继承的攻击面,并将web3特有覆盖细分为五大攻击面:生产与自动化运维、签名意图、审批与提现链、资金逻辑及链上交易与已部署合约。

机构级区块链渗透测试的交战规则与生产安全

机构级区块链渗透测试的交战规则与生产安全

涉及签名、提现和账本系统的渗透测试需事先充分准备。本文介绍工作流程:将业务决策转化为目标、范围、责任人及授权访问;在《交战规则》文档中记录授权、允许技术、操作限制、禁止行为、沟通及证据处理;通过可衡量的停止标准、监控、变更协调和指定暂停权限来保护线上服务安全,最终通过整改与复测将发现转化为经验证的控制措施。

什么是区块链渗透测试?定义与边界
Security Services

什么是区块链渗透测试?定义与边界

区块链渗透测试尚无公认定义,常与审计、扫描、漏洞赏金混淆。本文提出工作定义:在约定范围与规则下,对运行系统进行对抗性实测,验证可利用路径与控制链,并指出web3新增的资金处理威胁模型,其关键组合缺口在于链下到链上的交接。文章梳理该链条的五项可测能力,并将相近目标分流至代码审计、钱包安全审计、web3安全测试、扫描与漏洞赏金。

Web3 最佳安全审计方

在上线之前验证设计、代码与业务逻辑,对标业内最高安全标准。

BlockSec 审计