消失的负债 —— Notional Finance 被黑分析

作者:慢雾科技 发布:2026-09-07 11:00 收录:2026-09-08 10:31 7 次阅读 约 1864 字
摘要:攻击者利用 Escrow 合约类型转换中的溢出漏洞,将 2^128 的负债截断为 0,绕过自由抵押品检查并完成获利。
推荐理由:本文涵盖「Notional Finance」、「去中心化借贷」、「fCash」等多个主题,重点关注 Notional Finance。

作者:九九

编辑:77


背景


2026 年 9 月 4 日,知名去中心化借贷平台 Notional Finance 遭到攻击,损失约 173 万美元。以下是慢雾安全团队针对本次攻击事件的具体分析:

前置知识


在 Notional Finance V1 中,fCash 可以理解成带到期日的现金债权。cash receiver 在到期时收款,cash payer 在到期时付款。协议把两种仓位都记在 Portfolio 里,再通过自由抵押品检查判断一个账户是否可以建立仓位的健康程度。


ERC1155Trade 的 safeTransferFrom 在这里承担了资产记账功能。传入的资产类型属于 cash receiver 时,合约会调用 Portfolios.mintfCashPair,同时给 payer 记上一笔负债,给 receiver 记上一笔等额债权。这个调用看起来像 ERC1155 转账,结果却是一次 fCash 成对铸造。


新仓位写入 Portfolio 后,需要进行自由抵押品的计算和检查。RiskFramework 合约会先把 payer 的 fCash 负债汇总成有符号的 int256,再把各币种余额换算成 ETH 进行汇总。负数代表需要支付的现金,正数代表账户拥有的现金或债权。这个计算结果决定了交易能不能允许进行。


仓位到期后,Portfolio 会调用 Escrow 的 portfolioSettleCash 函数,把到期的 fCash 变成现金余额。账户再通过提款函数从 Escrow 取出对应的 DAI 或 USDC 资产。

根本原因


本次攻击核心漏洞在于 Escrow 实现合约所使用的 ExchangeRate._convertToETH,其中有一处代码会把有符号整数的绝对值直接转换成 uint128。

uint128 absBalance = uint128(balance.abs());// balance.abs() 超过 uint128 时,转换结果会静默截断
int256 result = int256(    SafeCast.toUint128(rate.mul(absBalance)... /* 后续换算 */));
return balance > 0 ? result : result.neg();

攻击者先建立了两笔相同 payer 的负债仓位,金额分别是 1 和 uint128.max。两笔负债相加后正好得到 2^128。由于 payer 在风险计算中付款是记为负数的,所以 Escrow 的 convertBalancesToETH 传入的关键参数则是 -2^128。


但是本合约所使用的 Solidity 版本是 0.6.x, 所以其绝对值 2^128 被强制转换成 uint128 后会被溢出变成 0,这笔负债也就没有进入 ETH 计价结果。


最后在 mintfCashPair 函数中只要求 payer 的自由抵押品大于或等于零,于是在结果溢出为 0 后可以通过该项检查,一个没有足够资产覆盖负债的账户仍然可以建立仓位。

攻击步骤分析


1. 在前置交易(0xe1589a19…d60a)中,攻击者首先创建了多个辅助合约,并调用 ERC1155Trade 合约的 setApprovalForAl 函数为辅助合约授权;此外还提前查询了两个 CashMarket 的到期日,计算好后续操作中需要传入的三种 AssetId 参数的值。



2. 紧接着,攻击者通过调用 ERC1155Trade 合约的 safeTransferFrom 函数铸造一笔数额为 1 的 fCash pair,对应债券资产的 cashGroupId 为 2,到期日时间戳为 1788480000(2026 年 9 月 4 日 8 点整)。其中会调用内部函数 _upsertAsset 对 from 地址(攻击合约)和 to 地址(接收合约 1)分别更新其负债和预期收入。



在 pair 铸造完成后,Portfolios 会立即检查攻击合约的自由抵押品,而由于第一次铸造的数量过小,在当前汇率和精度下换算成 ETH 后会被舍入为零,所以第一次检查能够通过。


3. 攻击者随后再次调用 ERC1155Trade 合约的 safeTransferFrom 函数铸造一笔数额为 uint128.max 的 fCash pair,这一次对应的债券资产的 cashGroupId 为 2,但是到期日时间戳却是 1796256000(2026 年 12 月 3 日 8 点整)。


这里有个细节,攻击者分别给两个不同的地址铸造了到期日不同的债券资产。这是因为在对 from 地址更新负债的时候,如果债券资产是相同的话则会直接对其进行累加,会导致在处理加法时因为溢出而 revert(合约使用了 safeMath 库)。



4. 在第二次铸造对 from 地址(攻击合约)和 to 地址(接收合约 2)更新完负债和预期收入后,mintfCashPair 函数会调用 freeCollateral 函数来计算 from 地址的净抵押品头寸并检查其需要大于等于 0 才是健康的。



其中会先在 RiskFrameork 合约中的 getRequirement 函数里对 from 地址的两次铸造的负债总和进行相加:



其最终结果为 -2^128,接着会调用 Escrow 合约的 convertBalancesToETH 函数将这个数值转换成 ETH 计价,而 convertBalancesToETH 函数又会调用 ExchangeRate 库的 _convertToETH 函数进行计算处理:




跟进到 _convertToETH 函数中,可以发现其会先将 balance 取绝对值后再调用 uint128 将 256 位转换成 128 位,而上面 from 的负债总和数值为 -2^128,取绝对值后的 2^128 在进行 uint128() 强转换时就会导致超出上限而溢出为 0。这就意味着协议误认为 from 地址的自由抵押品头寸为 0 是健康的,从而通过了 mintfCashPair 函数中的最终检查。



5. 之后接收合约 2 把其仓位又拆分给了另外的两个辅助合约,这两个 safeTransferFrom 仍然会进入 mintfCashPair 函数。但是因为此时的 payer 是第二次铸造时的接收合约 2,其持有前一步得到的巨大的预期收入仓位,风险计算将它视为正向债权从而通过了抵押品检查,两个辅助合约因此获得了到期后可以换成现金的 receiver fCash。



6. 攻击者随后发起了第二笔正式的获利交易,在这笔交易中,分别为上一笔交易的两个持有仓位的辅助合约结算其对应的债权,增加对应资产的现金余额,之后调用 Escrow 合约的 withdraw 函数将其资产提款获利。




总结


这次攻击事件的关键点在于攻击者先利用两个单独的仓位,把 payer 负债之和凑成 2^128,再让 Escrow 把这笔负债换算成 ETH。利用类型转换中的溢出漏洞把结果截断成零,风险检查因此放行通过。


慢雾安全团队建议项目方在进行类型转换前必须做范围检查,超过 type(uint128).max 的结果直接拒绝,与此同时,所有涉及有符号金额、精度缩放和资产面额的边界都应加入极值测试,尤其要覆盖 uint128.max、uint128.max + 1 和负数绝对值这几类输入。可以参考或使用 Openzeppelin 的 SafeCast 库进行转换处理。

往期回顾

威胁情报|iOS Safari DarkSword 窃取钱包资产

从“谁在充值”到“钱从哪里来”:OKX 风控策略背后的链上资金来源风险

慢雾:MistTrack & SlowMist KYT 合作伙伴计划正式启动

“灰犀牛”与“黑天鹅”:慢雾创始人余弦谈加密世界安全风险与防护

活动回顾|慢雾参与香港多场 Web3 活动,分享安全与合规实践

慢雾导航


慢雾科技官网

https://www.slowmist.com/


慢雾区官网

https://slowmist.io/


慢雾 GitHub

https://github.com/slowmist


Telegram

https://t.me/slowmistteam


Twitter

https://twitter.com/@slowmist_team


Medium

https://medium.com/@slowmist


知识星球

https://t.zsxq.com/Q3zNvvF

转载声明:本文转载自原发布平台 (作者:慢雾科技), 原文标题《消失的负债 —— Notional Finance 被黑分析》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。